Skip to main content

Convert Ory Sessions to JSON Web Tokens

Ory provides a robust session management system that uses cookies for browser clients and API tokens for API clients. It also supports converting a session into a JSON Web Token (JWT). A JWT can be a better option in scenarios such as:

  • Making cross-origin (CORS) requests where including the Ory session cookie or session token is difficult or not possible.
  • Representing a signed-in user with a JWT.
  • Integrating with third-party services, such as Zendesk SSO JWTs.
  • Reducing the number of calls to Ory's APIs.

This guide explains how to convert Ory sessions into JSON Web Tokens (JWTs).

End-to-end example

Let's look at an end-to-end example. This guide assumes that you already have an Ory Network project running. If not, create a new project now. First we need to create a JSON Web Key set and store it locally:

ory create jwk some-example-set \
--alg ES256 --project $PROJECT_ID --format json-pretty \
> es256.jwks.json

Next, we need to create a Jsonnet template that will be used to modify the claims of the JWT:

claims.jsonnet
local claims = std.extVar('claims');
local session = std.extVar('session');

{
claims: {
iss: claims.iss + "/additional-component",
schema_id: session.identity.schema_id,
session: session,
}
}

The easiest way to supply these files to Ory Network is to base64-encode them:

JWKS_B64_ENCODED=$(cat es256.jwks.json | base64 -w 0)
JSONNET_B64_ENCODED=$(cat claims.jsonnet | base64 -w 0)

Next, we configure our Ory Network project's tokenizer templates. The key we choose here is jwt_example_template1. We supply that template with the base64-encoded files from above:

ory patch identity-config --project <project-id> --workspace <workspace-id> \
--add '/session/whoami/tokenizer/templates/jwt_example_template1={"jwks_url":"base64://'$JWKS_B64_ENCODED'","claims_mapper_url":"base64://'$JSONNET_B64_ENCODED'","ttl":"10m"}' \
--format yaml

Great! Everything is set up! Let's convert an Ory Session to a JWT:

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

const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)

export async function toSessionWithJwt(sessionId: string) {
const session = await frontend.toSession({
tokenize_as: "jwt_example_template1",
})
const jwt = session.tokenized
return jwt
}

To verify the resulting JSON Web Token, export the public key from the JSON Web Key Set and use it to verify the token:

ory get jwk some-example-set \
--public \
--project $PROJECT_ID --format json-pretty \
> es256-public.jwks.json

JSON Web Token templates

Now that you have seen how this feature works in practice, let's look at how to configure it in detail. Before issuing a JWT for a Ory Session, you need to define one or more Ory Session tokenizer templates. A template has a unique key, a claims template, a TTL, and a URL where the cryptographic keys (JSON Web Key Sets) are fetched from:

session:
whoami:
tokenizer:
templates:
jwt_template_1:
jwks_url: base64://... # A JSON Web Key Set (required)
claims_mapper_url: base64://... # A JsonNet template for modifying the claims
ttl: 1m # 1 minute (defaults to 10 minutes)
another_jwt_template:
jwks_url: base64://... # A JSON Web Key Set

JSON Web Token claim mapper

You can customize the JSON Web Token claims by providing a JsonNet template to claims_mapper_url.

info

The sub claim can't be customized and is always set to the Ory Session's IdentityID.

The template has access to these variables:

  • claims: The default claims the token has per default. You can modify these claims but not remove them:
    • jti: A unique UUID v4 value.
    • iss: The project's slug url (https://$PROJECT_SLUG.projects.oryapis.com).
    • exp: The token's expiry which uses the template's ttl value.
    • sub: The Ory Session's IdentityID.
    • sid: The Ory Session's ID.
    • nbf: The time when the token becomes valid ("now").
    • iat: The time when the token was issued at ("now").
  • session: Contains the Ory Session's data. See the Ory Session documentation for more information.

The template must return a JSON object. For example:

local claims = std.extVar('claims');
local session = std.extVar('session');

{
claims: {
foo: "baz",
sub: "this can not be overwritten and will always be session.identity.id",
schema_id: session.identity.schema_id,
aal: session.authenticator_assurance_level,
second_claim: claims.exp,
// ...
}
}

JSON Web Token signing key(s)

The jwks_url must contain a JSON Web Key Set. All common cryptographic algorithms for JSON Web Tokens are supported such as ES256, RS256, RS512, and others.

info

Ory recommends to use ES256 or ES512 for signing JSON Web Tokens. Avoid symmetric algorithms such as the HS family (HS256, HS512, etc).

To generate test-keys you can use a service such as mkjwk.org. To generate keys for production, use the Ory CLI:

ory create jwk some-example-set \
--alg ES256 --project $PROJECT_ID --format json-pretty

{
"set": "example-key-set",
"keys": [
{
"alg": "ES256",
"crv": "P-256",
"d": "XdO-4OkdDxsOhU_XwYFAzEg1Z3DfQ8LhwivJeFq-ppo",
"kid": "3045631b-95a8-433c-ab54-93fa52a55ea8",
"kty": "EC",
"use": "sig",
"x": "AAYxrjPNt6M-XBY1H57Mc_6moiETkg_Cf2egHXPOEGo",
"y": "mNX9UCBa82GNrvIIHFFNxsw-LPKksbwCMoaIybyWMEY"
}
]
}

If the key set contains more than one key, the first key in the list will be used for signing:

{
set: "example-key-set",
keys: [
// This key will be used for signing:
{
alg: "ES256",
// ...
},
{
alg: "ES256",
// ...
},
],
}

Customizing JWT claims with webhooks

Sometimes, Jsonnet scripting is not enough to customize the JWT. A network call to a different service is necessary. A typical use case is adding custom claims to the tokens based on a separate database or business logic.

This is possible by registering a webhook endpoint in the configuration. This is all done in a separate endpoint: /sessions/whoami-jwt/{templateName}. Before the token is issued to the client, Ory will call your HTTPS endpoint with information about the client requesting the token. Note that the template name is mandatory. This way, your application can at runtime use different template names and thus third-party endpoints at will.

note

Be aware that this approach is easy but also has numerous disadvantages:

  • Added request latency: if the third-party service takes 100 milliseconds to respond to the request, that means that the response time for the /sessions/whoami-jwt/{templateName} increases by that much
  • If the third-party service is not available, authentication will fail for the user.

Thus, we recommend creating the JWT yourself if you need to add a lot of dynamic claims.

Alternatively and preferably, you can use distributed/aggregated claims.

Your endpoint's response to the webhook will be used to customize the JWT token that Ory issues to the client.

Using webhooks is supported for all flows.

If your endpoint is gated behind authentication, ensure that the configuration contains the correct credentials. They will be sent in the request when contacting the endpoint.

note

The webhook is called before any other logic is executed. If the webhook execution fails, for example if your endpoint is unreachable or responds with an HTTP error code, the token exchange will fail for the client.

Configuration

Use the Ory CLI to register your webhook endpoint:

ory patch identity-config --project <project-id> --workspace <workspace-id> \
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/url="https://my-example.app/token-hook"' \
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/type="api_key"' \
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/in="header"' \
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/name="X-API-Key"' \
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/value="MY API KEY"' \
--format yaml

Or use the YAML configuration:


session:
whoami:
tokenizer:
templates:
jwt_template_1:
jwks_url: base64://... # A JSON Web Key Set (required)
ttl: 1m # 1 minute (defaults to 10 minutes)
claims_hook:
url: https://my-example.app/token-hook
auth:
type: api_key
config:
in: header
name: X-API-Key
value: "MY API KEY"
another_jwt_template:
jwks_url: base64://... # A JSON Web Key Set
# [...]

Webhook payload

Ory will perform a POST request with a JSON payload towards your endpoint as configured, e.g. http://svc.example.com/jwt-webhook.

Example token webhook request payload
{
"request_headers": {
"X-Claim-Name": ["a custom claim"]
},
"request_method": "GET",
"request_url": "http://example.com/sessions/whoami-jwt/template_1",
"request_cookies": {},
"session": {
"id": "432caf86-c1d8-401c-978a-8da89133f78b",
"active": true,
"expires_at": "2025-08-22T16:29:26.579741+02:00",
"authenticated_at": "2025-08-21T16:29:26.579741+02:00",
"authenticator_assurance_level": "aal1",
"authentication_methods": [
{
"method": "password",
"aal": "aal1",
"completed_at": "2025-08-21T14:29:26.899803Z"
}
],
"issued_at": "2025-08-21T16:29:26.579741+02:00",
"identity": {
"id": "7458af86-c1d8-401c-978a-8da89133f78b",
"external_id": "external-id",
"schema_id": "default",
"schema_url": "http://localhost/schemas/ZGVmYXVsdA",
"state": "active",
"state_changed_at": "2025-08-21T14:29:26.899788Z",
"traits": {},
"metadata_public": null,
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "2025-08-21T16:29:26.900034+02:00",
"organization_id": null
},
"devices": [
{
"id": "00000000-0000-0000-0000-000000000000",
"ip_address": "192.0.2.1:1234",
"user_agent": null,
"location": ""
}
],
"tokenized": "eyJhbGciOiJFUzUxMiIsImtpZCI6ImJjN2Y3YWZjLTY3NDItNDI3Yy1iYjllLTE2NGZlMGY4YjZhNyIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiZXhwIjoxNjc1MjA5NjYwLCJleHRlcm5hbF9pZCI6ImV4dGVybmFsLWlkIiwiZm9vIjoiYmFyIiwiaWF0IjoxNjc1MjA5NjAwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0LyIsImp0aSI6IjYwZjdjOGU2LTIxYzMtNGExYi1hMTBhLTRjZjUyNmVhOWMzOCIsIm5iZiI6MTY3NTIwOTYwMCwic2NoZW1hX2lkIjoiZGVmYXVsdCIsInNlY29uZF9jbGFpbSI6MTY3NTIwOTY2MCwic2lkIjoiNDMyY2FmODYtYzFkOC00MDFjLTk3OGEtOGRhODkxMzNmNzhiIiwic3ViIjoiZXh0ZXJuYWwtaWQifQ.AXSrbfjtCvufuZEosTeHbS_oo01MiwdaB3ebS96pb9fO51QkC0rQHadY2hC3Ig4SP2x60NMl-Ff5mlEp4QTY9tPIATwFPbXFs-dBCKlZsLk7RA_s2fi4JXTQJU2WbxdHNGn_W3tL4IsTIQLPrrxXd301c72kDc4TVhLX99kIlasheBas"
},
"claims": {
"exp": 1675209660,
"iat": 1675209600,
"iss": "http://localhost/",
"jti": "eecab9dd-86fe-469a-8c04-ed7e103aea1e",
"nbf": 1675209600,
"sid": "432caf86-c1d8-401c-978a-8da89133f78b",
"sub": "7458af86-c1d8-401c-978a-8da89133f78b"
}
}

Fields prefixed with request_ contain information from the client's request to the /sessions/whoami-jwt/{templateName} endpoint.

The HTTP headers, present in the client request and that are in the allow-list (from the configuration field clients.web_hook.header_allowlist), will be forwarded to the webhook endpoint. In the above example, X-Claim-Name is such a HTTP header.

Responding to the webhook

When handling the webhook in your endpoint, use the request payload to decide how Ory should proceed in the token exchange with the client.

To accept the token exchange without modification, return a 204 or 200 HTTP status code without a response body.

To deny the token exchange, reply with a 403 HTTP status code.

To accept the claims without modification, return an empty body with a 204 status code.

To modify the claims of the issued tokens and instruct Ory Identities to proceed with the token exchange, return 200 with a JSON response body containing the claims that the final JWT should contain:

{
"claims": {
"name": "John Doe",
"admin": true
}
}

For the happy path, the response body must be an object containing a key claims, whose value is a map of string keys to any value. Each key-value in the claims object will be added as-is to the final JWT claims.

Responding with any other HTTP status code will abort the token exchange toward the client with an error message. In case of a 40x or 50x response code from your endpoint, a response body of any shape can be sent (for example containing an error message or code) and this response body will be included in the error sent back to the client on a best effort basis.

Updated tokens

Tokens issued by Ory to the client will contain the data from your webhook response:

{
"admin": true,
"exp": 1675209660,
"iat": 1675209600,
"iss": "http://localhost/",
"jti": "d4ef10de-bffd-48cb-a9cf-3cab91d4f5c2",
"name": "John Doe",
"nbf": 1675209600,
"sid": "432caf86-c1d8-401c-978a-8da89133f78b",
"sub": "7458af86-c1d8-401c-978a-8da89133f78b"
}
note

You cannot override the token subject.