In some cases, it’s necessary to act on behalf of a user. While the Auth service doesn’t support this directly, it’s not difficult to implement using a serverless function. Below is an example of a function that generates a valid access token for your application with customized values. For details, see the function’s docstring.

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.local.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"
}