Skip to content

Custom JWTs

JWT custom tokens user impersonation serverless functions access tokens Hasura claims

In some cases you need to act on behalf of a user — for example, to impersonate a user from an admin panel or to generate tokens with custom claims. The Auth service doesn’t support this directly, but you can implement it with a function.

npm install jsonwebtoken
npm install -D @types/jsonwebtoken

Create a file at ./functions/custom-jwt.ts:

import type { Request, Response } from 'express'
import process from 'node:process'
import jwt from 'jsonwebtoken'
const getJwt = (req: Request): string | null => {
const authHeader = req.headers.authorization
if (!authHeader) return null
const parts = authHeader.split(' ')
if (parts.length !== 2 || !/^Bearer$/i.test(parts[0])) return null
return parts[1]
}
const jwtIsAuthorized = (req: Request, key: string): string => {
const token = getJwt(req)
if (!token) return ''
try {
const decoded = jwt.verify(token, key)
const claims = decoded['https://hasura.io/jwt/claims']
if (!claims?.['x-hasura-allowed-roles']) return ''
if (
claims['x-hasura-allowed-roles'].includes('admin') ||
claims['x-hasura-allowed-roles'].includes('operator')
) {
return decoded.sub
}
return ''
} catch {
return ''
}
}
export default (req: Request, res: Response) => {
let authorizedCaller = ''
if (req.headers['x-hasura-admin-secret'] === process.env.HASURA_GRAPHQL_ADMIN_SECRET) {
authorizedCaller = 'admin'
}
const jwtSecret = JSON.parse(process.env.NHOST_JWT_SECRET)
if (!authorizedCaller) {
authorizedCaller = jwtIsAuthorized(req, jwtSecret.key)
}
if (!authorizedCaller) {
return res.status(401).json({ message: 'Unauthorized' })
}
const { userId, defaultRole, allowedRoles } = req.body
if (!userId || !defaultRole || !allowedRoles) {
return res.status(400).json({ message: 'Bad request' })
}
const token = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60,
'https://hasura.io/jwt/claims': {
'x-hasura-allowed-roles': allowedRoles,
'x-hasura-default-role': defaultRole,
'x-hasura-user-id': userId,
'x-hasura-user-is-anonymous': 'false',
'x-hasura-on-behalf-of': authorizedCaller,
},
iat: Math.floor(Date.now() / 1000),
iss: 'custom-lambda',
sub: userId,
},
jwtSecret.key,
)
res.status(200).json({ accessToken: token })
}

The function accepts two authorization methods:

  • Admin secret: Send the x-hasura-admin-secret header
  • JWT: Send a valid JWT with the admin or operator role
FieldTypeDescription
userIdstringUser ID to generate the token for
defaultRolestringDefault role for the user
allowedRolesstring[]Roles the user can assume
curl -X POST \
-H "Content-Type: application/json" \
-H "x-hasura-admin-secret: nhost-admin-secret" \
-d '{"userId": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883", "defaultRole": "user", "allowedRoles": ["user", "me"]}' \
https://local.functions.local.nhost.run/v1/custom-jwt

The response contains a signed JWT with the custom claims, including x-hasura-on-behalf-of indicating who initiated the request.