In some cases it is necessary to act on behalf of a user. While the Auth service doesn’t allow that it is not difficult to implement such functionality as a serverless function. Below you can find an example of a function that can generate a valid access token for your application with customized values. For details read the docstring in the function itself.

Feel free to adapt to your needs.

Dependencies

npm install jsonwebtoken

Function

Create a file under functions (for instance /functions/custom-jwts.ts), with the following contents:

import { Request, Response } from 'express'
import process from 'process'
import jwt from 'jsonwebtoken'

// function to extract jwt from the request
const getJwt = (req: Request): string | null => {
    const authHeader = req.headers.authorization
    if (!authHeader) {
        return null
    }

    const parts = authHeader.split(' ')
    if (parts.length !== 2) {
        return null
    }

    const [scheme, token] = parts
    if (!/^Bearer$/i.test(scheme)) {
        return null
    }

    return token
}

// validate jwt token is valid and caller is either an admin or an operator
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 || !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 (e) {
        return ""
    }
}

/*
This is a sample function that generates a JWT token to impersonate users.

Authorization:

- send a valid JWT token with the request. The token should have the `admin` or `operator` role.
- send the `x-hasura-admin-secret` header with the request

Body:

A json object with the following keys:

- `userId` (string): the user id for which the token is generated
- `defaultRole` (string): the default role for the userId
- `allowedRoles` (array of strings): the roles that the userId can assume

Returns:

A json object with the following keys:

- `accessToken` (string) - The generated access token


In addition to the typical JWT claims generated by Nhost, the token generated by this function will have the following claims:

- `x-hasura-on-behalf-of`: the user id of the caller or `admin` if the caller used the `x-hasura-admin-secret` header

You are free to modify this function to suit your needs.
*/
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' })
    }

    // extract from json in the body
    const {userId, defaultRole, allowedRoles} = req.body
    if (!userId || !defaultRole || !allowedRoles) {
        return res.status(400).json({ message: 'Bad request' })
    }

    let token = jwt.sign({
      "exp": Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour
      "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,
        },
    )
}

Now you can call it like:

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.nhost.run/v1/custom-jwt
{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTcxNDIyMTMsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIiwibWUiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLXVzZXItaWQiOiJGRkFCNTM1NC1DNUVCLTQyQzEtOEJDMy1BRDIxRDIyOTc4ODMiLCJ4LWhhc3VyYS11c2VyLWlzLWFub255bW91cyI6ImZhbHNlIiwieC1oYXN1cmEtb24tYmVoYWxmLW9mIjoiYWRtaW4ifSwiaWF0IjoxNzE3MTM4NjEzLCJpc3MiOiJjdXN0b20tbGFtYmRhIiwic3ViIjoiRkZBQjUzNTQtQzVFQi00MkMxLThCQzMtQUQyMUQyMjk3ODgzIn0.bRhzJvXMdkQA8aXPH95uMT17WHED2rSRq3gE21Vp3Ak"}

The new token should be a valid token for your application with the custom values requested:

{
  "exp": 1717141288,
  "https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": [
      "a",
      "b"
    ],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883",
    "x-hasura-user-is-anonymous": "false",
    "x-hasura-on-behalf-of": "admin"
  },
  "iat": 1717137688,
  "iss": "custom-lambda",
  "sub": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883"
}