Skip to content

Tokens and Scopes

oauth2 tokens scopes jwt access token id token refresh token introspection revocation graphql hasura
TokenFormatDefault LifetimeDescription
Access TokenRSA-signed JWT15 minutesUsed to access protected resources
ID TokenRSA-signed JWTSame as access tokenContains user identity claims (OIDC)
Refresh TokenUUID30 daysUsed to obtain new access and ID tokens

Access tokens are RSA-signed JWTs. They contain standard OAuth2 claims:

{
"iss": "https://your-project.auth.nhost.run/v1",
"sub": "f5765cb0-1c45-4b6e-8a30-0b2abc1a2f3d",
"aud": "nhoa_a1b2c3d4e5f67890",
"iat": 1234567890,
"exp": 1234568790,
"scope": "openid profile email"
}

The JWT header includes typ: JWT and kid identifying the signing key. The signing algorithm depends on your RSA key configuration (RS256, RS384, or RS512). See Verify a JWT for how to verify tokens using the JWKS endpoint.

When the graphql scope is requested, the access token includes GraphQL-compatible claims. This allows the access token to be used directly with your GraphQL API for authorization.

{
"iss": "https://your-project.auth.nhost.run/v1",
"sub": "f5765cb0-1c45-4b6e-8a30-0b2abc1a2f3d",
"aud": "nhoa_a1b2c3d4e5f67890",
"iat": 1234567890,
"exp": 1234568790,
"scope": "openid profile email graphql",
"https://hasura.io/jwt/claims": {
"x-hasura-user-id": "f5765cb0-1c45-4b6e-8a30-0b2abc1a2f3d",
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": ["user", "me"],
"x-hasura-my-custom-claim": "custom value"
}
}

Without the graphql scope, the https://hasura.io/jwt/claims namespace is omitted from the access token.

The ID token is issued when the openid scope is requested. It contains identity claims about the authenticated user, controlled by the scopes granted:

ClaimDescription
issIssuer URL
subUser UUID
audClient ID
iatIssued at timestamp
expExpiration timestamp
auth_timeWhen the user authenticated
at_hashAccess token hash (when access token is issued alongside)
ScopeClaimsCondition
profilename, picture, localeOnly if the user has these values set
emailemail, email_verifiedOnly if the user has an email
phonephone_number, phone_number_verifiedOnly if the user has a phone number

The nonce claim is included in the ID token if a nonce parameter was provided in the authorization request. It is not included on refresh.

The UserInfo endpoint returns user claims filtered by the scopes of the access token.

const { body: userinfo } = await nhost.auth.oauth2UserinfoGet({
headers: { Authorization: `Bearer ${accessToken}` },
});

Example response (with openid profile email scopes):

{
"sub": "f5765cb0-1c45-4b6e-8a30-0b2abc1a2f3d",
"name": "Jane Doe",
"picture": "https://www.gravatar.com/avatar/...",
"locale": "en",
"email": "jane@example.com",
"email_verified": true
}

Both GET and POST methods are supported, per the OIDC specification.

Refresh tokens are UUID strings (not JWTs). They implement token rotation: each time a refresh token is used, it is deleted and a new one is issued. The new token inherits the original scopes.

const { body: tokens } = await nhost.auth.oauth2Token({
grant_type: 'refresh_token',
refresh_token: currentRefreshToken,
client_id: clientId,
client_secret: clientSecret, // required for confidential clients
});
// tokens.refresh_token is a NEW token — the old one is now invalid

The response format is the same as the initial token exchange — it includes a new access token, ID token (if openid scope), and refresh token.

Check whether a token is active by calling the introspection endpoint. Client authentication is required.

const { body: result } = await nhost.auth.oauth2Introspect({
token: tokenToCheck,
token_type_hint: 'access_token', // or 'refresh_token'
client_id: clientId,
client_secret: clientSecret, // required for confidential clients
});

Active token response:

{
"active": true,
"sub": "f5765cb0-1c45-4b6e-8a30-0b2abc1a2f3d",
"client_id": "nhoa_a1b2c3d4e5f67890",
"scope": "openid profile email",
"exp": 1234568790,
"iat": 1234567890,
"iss": "https://your-project.auth.nhost.run/v1",
"token_type": "access_token"
}

Inactive token response:

{
"active": false
}

Revoke a refresh token to immediately invalidate it. The endpoint always returns 200 OK regardless of whether the token existed, per the RFC 7009 security recommendation.

await nhost.auth.oauth2Revoke({
token: refreshToken,
token_type_hint: 'refresh_token',
client_id: clientId,
client_secret: clientSecret, // required for confidential clients
});