Skip to content

Commit 9d13fea

Browse files
gaultierunatasha8
andauthored
docs: document jwt hook claims (#2281)
Update docs/identities/session-to-jwt-cors.mdx Co-authored-by: unatasha8 <[email protected]>
1 parent 7c82d74 commit 9d13fea

File tree

1 file changed

+278
-2
lines changed

1 file changed

+278
-2
lines changed

docs/identities/session-to-jwt-cors.mdx

Lines changed: 278 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ ory create jwk some-example-set \
2727
> es256.jwks.json
2828
```
2929

30-
Next, we need to create a JsonNet template that will be used to modify the claims of the JWT:
30+
Next, we need to create a Jsonnet template that will be used to modify the claims of the JWT:
3131

3232
```jsonnet title="claims.jsonnet"
3333
local claims = std.extVar('claims');
@@ -42,7 +42,7 @@ local session = std.extVar('session');
4242
}
4343
```
4444

45-
The easiest way to supplies these files to Ory Network is to base64-encode them:
45+
The easiest way to supply these files to Ory Network is to base64-encode them:
4646

4747
```shell
4848
JWKS_B64_ENCODED=$(cat es256.jwks.json | base64 -w 0)
@@ -219,3 +219,279 @@ If the key set contains more than one key, the first key in the list will be use
219219
],
220220
}
221221
```
222+
223+
### Customizing JWT claims with webhooks
224+
225+
Sometimes, Jsonnet scripting is not enough to customize the JWT. A network call to a different service is necessary. A typical use
226+
case is adding custom claims to the tokens based on a separate database or business logic.
227+
228+
This is possible by registering a webhook endpoint in the configuration. This is all done in a separate endpoint:
229+
`/sessions/whoami-jwt/{templateName}`. Before the token is issued to the client, Ory will call your HTTPS endpoint with
230+
information about the client requesting the token. Note that the template name is mandatory. This way, your application can at
231+
runtime use different template names and thus third-party endpoints at will.
232+
233+
:::note
234+
235+
Be aware that this approach is easy but also has numerous disadvantages:
236+
237+
- Added request latency: if the third-party service takes 100 milliseconds to respond to the request, that means that the response
238+
time for the `/sessions/whoami-jwt/{templateName}` increases by that much
239+
- If the third-party service is not available, authentication will fail for the user.
240+
241+
Thus, we recommend creating the JWT yourself if you need to add a lot of dynamic claims.
242+
243+
Alternatively and preferably, you can use
244+
[distributed/aggregated claims](https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims).
245+
246+
:::
247+
248+
Your endpoint's response to the webhook will be used to customize the JWT token that Ory issues to the client.
249+
250+
Using webhooks is supported for all flows.
251+
252+
If your endpoint is gated behind authentication, ensure that the configuration contains the correct credentials. They will be sent
253+
in the request when contacting the endpoint.
254+
255+
:::note
256+
257+
The webhook is called before any other logic is executed. If the webhook execution fails, for example if your endpoint is
258+
unreachable or responds with an HTTP error code, the token exchange will fail for the client.
259+
260+
:::
261+
262+
#### Configuration
263+
264+
Use the Ory CLI to register your webhook endpoint:
265+
266+
```mdx-code-block
267+
<Tabs>
268+
<TabItem value="header-auth" label="With authentication in header" default >
269+
<CodeBlock language="shell">{
270+
`ory patch identity-config --project <project-id> --workspace <workspace-id> \\
271+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/url="https://my-example.app/token-hook"' \\
272+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/type="api_key"' \\
273+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/in="header"' \\
274+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/name="X-API-Key"' \\
275+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/value="MY API KEY"' \\
276+
--format yaml`}
277+
</CodeBlock>
278+
</TabItem>
279+
<TabItem value="cookie-auth" label="With authentication in cookie">
280+
<CodeBlock language="shell">{
281+
`ory patch identity-config --project <project-id> --workspace <workspace-id> \\
282+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/url="https://my-example.app/token-hook"' \\
283+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/type="api_key"' \\
284+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/in="cookie"' \\
285+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/name="X-Cookie-Name"' \\
286+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/auth/config/value="MY SECRET COOKIE"' \\
287+
--format yaml`}
288+
</CodeBlock>
289+
</TabItem>
290+
<TabItem value="no-auth" label="No authentication">
291+
<CodeBlock language="shell">{
292+
`ory patch identity-config --project <project-id> --workspace <workspace-id> \\
293+
--add '/session/whoami/tokenizer/templates/jwt_template_1/claims_hook/url="https://my-example.app/token-hook"' \\
294+
--format yaml`}
295+
</CodeBlock>
296+
</TabItem>
297+
</Tabs>
298+
```
299+
300+
Or use the YAML configuration:
301+
302+
```mdx-code-block
303+
<Tabs>
304+
<TabItem value="header-auth" label="With authentication in header" default >
305+
<CodeBlock language="yaml">{`
306+
session:
307+
whoami:
308+
tokenizer:
309+
templates:
310+
jwt_template_1:
311+
jwks_url: base64://... # A JSON Web Key Set (required)
312+
ttl: 1m # 1 minute (defaults to 10 minutes)
313+
// highlight-start
314+
claims_hook:
315+
url: https://my-example.app/token-hook
316+
auth:
317+
type: api_key
318+
config:
319+
in: header
320+
name: X-API-Key
321+
value: "MY API KEY"
322+
// highlight-end
323+
another_jwt_template:
324+
jwks_url: base64://... # A JSON Web Key Set
325+
# [...]
326+
`}
327+
</CodeBlock>
328+
</TabItem>
329+
<TabItem value="cookie-auth" label="With authentication in cookie">
330+
<CodeBlock language="yaml">{`
331+
session:
332+
whoami:
333+
tokenizer:
334+
templates:
335+
jwt_template_1:
336+
jwks_url: base64://... # A JSON Web Key Set (required)
337+
ttl: 1m # 1 minute (defaults to 10 minutes)
338+
// highlight-start
339+
claims_hook:
340+
url: https://my-example.app/token-hook
341+
auth:
342+
type: api_key
343+
config:
344+
in: cookie
345+
name: X-Cookie-Name
346+
value: "MY SECRET COOKIE"
347+
// highlight-end
348+
another_jwt_template:
349+
jwks_url: base64://... # A JSON Web Key Set
350+
# [...]
351+
`}
352+
</CodeBlock>
353+
</TabItem>
354+
<TabItem value="no-auth" label="No authentication">
355+
<CodeBlock language="yaml">{`
356+
session:
357+
whoami:
358+
tokenizer:
359+
templates:
360+
jwt_template_1:
361+
jwks_url: base64://... # A JSON Web Key Set (required)
362+
ttl: 1m # 1 minute (defaults to 10 minutes)
363+
// highlight-start
364+
claims_hook:
365+
url: http://svc.example.com/jwt-webhook
366+
// highlight-end
367+
another_jwt_template:
368+
jwks_url: base64://... # A JSON Web Key Set
369+
# [...]
370+
`}
371+
</CodeBlock>
372+
</TabItem>
373+
</Tabs>
374+
```
375+
376+
#### Webhook payload
377+
378+
Ory will perform a POST request with a JSON payload towards your endpoint as configured, e.g.
379+
`http://svc.example.com/jwt-webhook`.
380+
381+
```json title="Example token webhook request payload"
382+
{
383+
"request_headers": {
384+
"X-Claim-Name": ["a custom claim"]
385+
},
386+
"request_method": "GET",
387+
"request_url": "http://example.com/sessions/whoami-jwt/template_1",
388+
"request_cookies": {},
389+
"session": {
390+
"id": "432caf86-c1d8-401c-978a-8da89133f78b",
391+
"active": true,
392+
"expires_at": "2025-08-22T16:29:26.579741+02:00",
393+
"authenticated_at": "2025-08-21T16:29:26.579741+02:00",
394+
"authenticator_assurance_level": "aal1",
395+
"authentication_methods": [
396+
{
397+
"method": "password",
398+
"aal": "aal1",
399+
"completed_at": "2025-08-21T14:29:26.899803Z"
400+
}
401+
],
402+
"issued_at": "2025-08-21T16:29:26.579741+02:00",
403+
"identity": {
404+
"id": "7458af86-c1d8-401c-978a-8da89133f78b",
405+
"external_id": "external-id",
406+
"schema_id": "default",
407+
"schema_url": "http://localhost/schemas/ZGVmYXVsdA",
408+
"state": "active",
409+
"state_changed_at": "2025-08-21T14:29:26.899788Z",
410+
"traits": {},
411+
"metadata_public": null,
412+
"created_at": "0001-01-01T00:00:00Z",
413+
"updated_at": "2025-08-21T16:29:26.900034+02:00",
414+
"organization_id": null
415+
},
416+
"devices": [
417+
{
418+
"id": "00000000-0000-0000-0000-000000000000",
419+
"ip_address": "192.0.2.1:1234",
420+
"user_agent": null,
421+
"location": ""
422+
}
423+
],
424+
"tokenized": "eyJhbGciOiJFUzUxMiIsImtpZCI6ImJjN2Y3YWZjLTY3NDItNDI3Yy1iYjllLTE2NGZlMGY4YjZhNyIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiZXhwIjoxNjc1MjA5NjYwLCJleHRlcm5hbF9pZCI6ImV4dGVybmFsLWlkIiwiZm9vIjoiYmFyIiwiaWF0IjoxNjc1MjA5NjAwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0LyIsImp0aSI6IjYwZjdjOGU2LTIxYzMtNGExYi1hMTBhLTRjZjUyNmVhOWMzOCIsIm5iZiI6MTY3NTIwOTYwMCwic2NoZW1hX2lkIjoiZGVmYXVsdCIsInNlY29uZF9jbGFpbSI6MTY3NTIwOTY2MCwic2lkIjoiNDMyY2FmODYtYzFkOC00MDFjLTk3OGEtOGRhODkxMzNmNzhiIiwic3ViIjoiZXh0ZXJuYWwtaWQifQ.AXSrbfjtCvufuZEosTeHbS_oo01MiwdaB3ebS96pb9fO51QkC0rQHadY2hC3Ig4SP2x60NMl-Ff5mlEp4QTY9tPIATwFPbXFs-dBCKlZsLk7RA_s2fi4JXTQJU2WbxdHNGn_W3tL4IsTIQLPrrxXd301c72kDc4TVhLX99kIlasheBas"
425+
},
426+
"claims": {
427+
"exp": 1675209660,
428+
"iat": 1675209600,
429+
"iss": "http://localhost/",
430+
"jti": "eecab9dd-86fe-469a-8c04-ed7e103aea1e",
431+
"nbf": 1675209600,
432+
"sid": "432caf86-c1d8-401c-978a-8da89133f78b",
433+
"sub": "7458af86-c1d8-401c-978a-8da89133f78b"
434+
}
435+
}
436+
```
437+
438+
Fields prefixed with `request_` contain information from the client's request to the `/sessions/whoami-jwt/{templateName}`
439+
endpoint.
440+
441+
The HTTP headers, present in the client request and that are in the allow-list (from the configuration field
442+
`clients.web_hook.header_allowlist`), will be forwarded to the webhook endpoint. In the above example, `X-Claim-Name` is such a
443+
HTTP header.
444+
445+
#### Responding to the webhook
446+
447+
When handling the webhook in your endpoint, use the request payload to decide how Ory should proceed in the token exchange with
448+
the client.
449+
450+
To accept the token exchange without modification, return a `204` or `200` HTTP status code without a response body.
451+
452+
To deny the token exchange, reply with a `403` HTTP status code.
453+
454+
To accept the claims without modification, return an empty body with a 204 status code.
455+
456+
To modify the claims of the issued tokens and instruct Ory Identities to proceed with the token exchange, return `200` with a JSON
457+
response body containing the claims that the final JWT should contain:
458+
459+
```json
460+
{
461+
"claims": {
462+
"name": "John Doe",
463+
"admin": true
464+
}
465+
}
466+
```
467+
468+
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
469+
value. Each key-value in the `claims` object will be added as-is to the final JWT claims.
470+
471+
Responding with any other HTTP status code will abort the token exchange toward the client with an error message. In case of a 40x
472+
or 50x response code from your endpoint, a response body of any shape can be sent (for example containing an error message or
473+
code) and this response body will be included in the error sent back to the client on a best effort basis.
474+
475+
#### Updated tokens
476+
477+
Tokens issued by Ory to the client will contain the data from your webhook response:
478+
479+
```json
480+
{
481+
"admin": true,
482+
"exp": 1675209660,
483+
"iat": 1675209600,
484+
"iss": "http://localhost/",
485+
"jti": "d4ef10de-bffd-48cb-a9cf-3cab91d4f5c2",
486+
"name": "John Doe",
487+
"nbf": 1675209600,
488+
"sid": "432caf86-c1d8-401c-978a-8da89133f78b",
489+
"sub": "7458af86-c1d8-401c-978a-8da89133f78b"
490+
}
491+
```
492+
493+
:::note
494+
495+
You cannot override the token subject.
496+
497+
:::

0 commit comments

Comments
 (0)