@@ -27,7 +27,7 @@ ory create jwk some-example-set \
27
27
> es256.jwks.json
28
28
```
29
29
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:
31
31
32
32
``` jsonnet title="claims.jsonnet"
33
33
local claims = std.extVar('claims');
@@ -42,7 +42,7 @@ local session = std.extVar('session');
42
42
}
43
43
```
44
44
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:
46
46
47
47
``` shell
48
48
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
219
219
],
220
220
}
221
221
` ` `
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