Skip to main content

Opaque and JWT access tokens

JSON Web Tokens (JWTs) are a widely used format for representing claims securely between parties. They can be used as access tokens, which are used to grant access to protected resources, such as APIs. JWTs consist of three parts:

  • Header: contains metadata about the token, such as the algorithm used to sign it.
  • Payload: contains claims, which are statements about an entity (typically, the user) and additional data.
  • Signature: used to verify that the token was not tampered with.

Opaque access tokens versus JWT

By default, Ory issues opaque access tokens, which are random strings with a cryptographic signature that have no meaning or structure. Opaque tokens are stored in a database, and their validity is checked by performing a database lookup.

Here's an example of an opaque token:

ory_at_JGhESDjKfHMQ8Wcy0cC3.hIQxGmX37ydn8WmKAnlD3U

Opaque tokens are stored in a database, and their validity can be checked by performing a database lookup. This means that if you need to revoke an opaque token, you can simply delete it from the database. Once the token has been deleted, any subsequent attempts to use it will fail.

JWTs, on the other hand, are self-contained and do not require a database lookup to validate. Instead, JWTs contain a signature that can be verified to ensure that the token has not been tampered with. Because of this, revoking a JWT requires a different approach.

Overall, opaque tokens have an advantage when it comes to revocation, as they can be immediately revoked by deleting them from the database. JWTs, on the other hand, have a delay between being marked as invalid and actually becoming invalid, which can be a problem in some cases. However, it's worth noting that opaque tokens require a network roundtrip to the introspection API to validate, which can be slower than validating a JWT.

opaque tokensjwt tokens
revocationimmediateeventually
latency~50ms~1ms

Refresh tokens are always opaque

Ory uses opaque tokens for refresh tokens. Refresh tokens are used in OAuth 2.0 to obtain new access tokens once the original access token has expired. Refresh tokens are always opaque tokens in Ory because they must be immediately revocable if needed.

JWT access tokens

Global configuration

To configure Ory OAuth2 to issue JWT access tokens for all clients, you must update the configuration as Ory defaults to opaque tokens:

ory patch oauth2-config {project.id} \
--replace "/strategies/access_token=\"jwt\""

Per-client configuration

To configure Ory OAuth2 to issue JWT access tokens for a specific client, you'll need to update the client configuration as Ory defaults to opaque tokens:


import { Configuration, JsonPatch, OAuth2Api } from "@ory/client"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

export async function patchOAuth2Client(id: string, patches: JsonPatch[]) {
await ory.patchOAuth2Client({
id,
jsonPatch: [
...patches,
{
op: "replace",
path: "access_token_strategy",
value: "jwt",
},
],
})
}

The setting is also available through the Ory Console under the clients' settings.

Revoking JSON Web Tokens

One limitation of using JSON Web Tokens (JWTs) as access tokens is that they can't be revoked immediately after being issued. Instead, revoking a JWT requires either waiting for it to expire or using a blacklist or revocation list to mark the token as invalid.

However, there is another option for checking whether a JWT has been revoked: OAuth 2.0 token introspection. Token introspection is a feature of OAuth 2.0 that allows a client to query the authorization server to determine the validity and other metadata of an access token.

To use token introspection to check whether a JWT has been revoked, the client sends a request to the authorization server's introspection endpoint, providing the access token in question as a parameter. The introspection endpoint responds with information about the token, including its validity, expiry time, and any associated metadata.

Here's an example of a token introspection request using the SDK:


import { Configuration, OAuth2Api } from "@ory/client"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

export async function introspectToken(accessToken: string) {
const { data } = await ory.introspectOAuth2Token({ token: accessToken })
data.active // true or false
}

Limitations

While JWTs have many benefits as access tokens, there are also some limitations to consider:

  • OAuth 2.0 Access Tokens represent internal state but are public knowledge: An Access Token often contains internal data (such as session data) or other sensitive data (such as user roles and permissions) and is sometimes used as a means of transporting system-relevant information in a stateless manner. Therefore, making these tokens transparent (by using JSON Web Tokens as Access Tokens) comes with the risk of exposing this information, and with the downside of not storing this information in the OAuth 2.0 Access Token at all.
  • JSON Web Tokens can't hold secrets: Unless encrypted, JSON Web Tokens can be read by everyone, including third parties.
  • Access Tokens as JSON Web Tokens can't be revoked: You can revoke a JWT token, but the token is still considered valid until the token's expiry time is reached. If required, however, you can use the introspection API to check if the token was revoked.
  • Certain OpenID Connect features don't work: When using JSON Web Tokens as access tokens, certain OpenID Connect features might not work, such as the pairwise subject identifier algorithm.

JSON Web Token validation

You can validate JSON Web Tokens issued by Ory by pointing your jwt library (for example node-jwks-rsa) to:

http://{project.slug}.projects.oryapis.com/.well-known/jwks.json

All necessary keys are available there.

Default JSON Web Key Sets

In Ory, JSON Web Key (JWK) endpoints require a set-id. This set-id corresponds to the specific set of keys you want to interact with. If you're looking to interact with the default keys used by Ory, you can use the following set-id values:

  • hydra.openid.id-token: This set contains the keys used for signing OpenID Connect ID Tokens.
  • hydra.jwt.access-token: This set contains the keys used for signing JWT Access Tokens.

You can use these set-id values with the JWK endpoints to retrieve, rotate, or otherwise manage these default key sets. For example, to retrieve the OpenID Connect ID Token key set, you would use the GET /admin/keys/{set} endpoint with hydra.openid.id-token as the {set} parameter. This information is particularly useful when rotating keys, as described in the documentation on key rotation. Remember, these are the default key sets used by Ory. If you've configured Ory to use different key sets, you'll need to use the set-id values corresponding to your custom key sets.

Using the following code during, you can add custom claims to every access token during consent acceptance


import { Configuration, OAuth2Api } from "@ory/client"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

export async function acceptConsent(consentChallenge: string) {
const { data } = await ory.getOAuth2ConsentRequest({ consentChallenge })

return await ory
.acceptOAuth2ConsentRequest({
consentChallenge: consentChallenge,
acceptOAuth2ConsentRequest: {
session: {
access_token: {
some_custom_claim: "some_custom_value",
},
id_token: {
id_custom_claim: "some_value",
},
},
},
})
.then(({ data }) => data)
}

Which results in the following access token:

{
sub: "...",
// ...
ext: {
some_custom_claim: "some_custom_value",
},
// ...
}

If you want some_custom_claim to be added to the top level (instead of a nested level) in the access token, you need to modify the /oauth2/allowed_top_level_claims configuration:

note

Required JWT claims can't be overwritten by custom claims.

ory patch oauth2-config {project.id} \
--replace "/oauth2/allowed_top_level_claims=[\"some_custom_claim\"]"

Which results in an access token with the following structure:

{
"sub": "...",
// ...
"some_custom_claim": "some_custom_value",
"ext": {
"some_custom_claim": "some_custom_value"
}
// ...
}

Disable mirroring the claims under ext

By default, all custom claims will be mirrored in the ext claim (as is the case with the claim some_custom_claim in the example above).

Set oauth2.mirror_top_level_claims to false to disable mirroring the claims in the nested ext claim:

ory patch oauth2-config {project.id} \
--replace "/oauth2/mirror_top_level_claims=false"

This results in an access token with the following structure:

{
"sub": "...",
// ...
"some_custom_claim": "some_custom_value"
// no "ext" claim
// ...
}