Customizing claims with OAuth2 webhooks
You can modify aspects of the OpenID Connect and access tokens returned from Hydra's OAuth2 token endpoint. A typical use case is adding custom claims to the tokens issued by Ory OAuth2/Ory Hydra.
Customize the token response by registering a webhook endpoint in your OAuth2 configuration. Before the token is issued to the client, Ory will call your HTTPS endpoint with information about the OAuth client requesting the token.
When performing an Authorization Code flow information about the resource owner's session is also included in the webhook payload.
Your endpoint's response to the webhook will be used to customize the token that Ory issues to the OAuth client, and optionally overwrite the session data stored for the resource owner.
Using webhooks is supported for all grant types (flows).
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 OAuth client.
Configuration
Use the Ory CLI to register your webhook endpoint:
- with authentication in header
- with authentication in cookie
- no authentication
ory patch oauth2-config --project <project-id> --workspace <workspace-id> \
--add '/oauth2/token_hook/url="https://my-example.app/token-hook"' \
--add '/oauth2/token_hook/auth/type="api_key"' \
--add '/oauth2/token_hook/auth/config/in="header"' \
--add '/oauth2/token_hook/auth/config/name="X-API-Key"' \
--add '/oauth2/token_hook/auth/config/value="MY API KEY"' \
--format yaml
ory patch oauth2-config --project <project-id> --workspace <workspace-id> \
--add '/oauth2/token_hook/url="https://my-example.app/token-hook"' \
--add '/oauth2/token_hook/auth/type="api_key"' \
--add '/oauth2/token_hook/auth/config/in="cookie"' \
--add '/oauth2/token_hook/auth/config/name="X-Cookie-Name"' \
--add '/oauth2/token_hook/auth/config/value="MY SECRET COOKIE"' \
--format yaml
ory patch oauth2-config --project <project-id> --workspace <workspace-id> \
--add '/oauth2/token_hook="https://my-example.app/token-hook"' \
--format yaml
Webhook payload
Ory will perform a POST request with a JSON payload towards your endpoint.
{
"session": {
"id_token": {
"id_token_claims": {
"jti": "",
"iss": "http://your-slug-xyz.projects.oryapis.com",
"sub": "subject",
"aud": ["app-client"],
"nonce": "",
"at_hash": "",
"acr": "1",
"amr": null,
"c_hash": "",
"ext": {}
},
"headers": {
"extra": {}
},
"username": "",
"subject": "foo"
},
"extra": {},
"client_id": "app-client",
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": []
},
"request": {
"client_id": "app-client",
"granted_scopes": ["offline", "openid", "hydra.*"],
"granted_audience": [],
"grant_types": ["authorization_code"],
"payload": {
"assertion": ["eyJhbGciOiJIUzI..."]
}
}
}
session
represents the OAuth2 session, along with the data that was passed to the
Accept Consent Request in the id_token
field (only
applicable to Authorization code flows).
request
contains information from the OAuth client's request to the token endpoint.
The request.payload.assertion
field will be populated for flows of the
urn:ietf:params:oauth:grant-type:jwt-bearer
grant type only, and contains the JWT
the client passed as the assertion
in their call to the token endpoint.
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 modify the claims of the issued tokens and instruct Hydra to proceed with the token exchange, return 200
with a JSON response
body:
{
"session": {
"access_token": {
"your:custom:access-token-claim": "any value you like",
"your:second:access-token-claim": 124390123
},
"id_token": {
"your:custom:id-token-claim": "another value",
"your:second:id-token-claim": 2394123
}
}
}
Responding with any other HTTP status code will abort the token exchange toward the OAuth2 client with an error message.
Updated tokens
Tokens issued by Ory to the OAuth2 client will contain the data from your webhook response:
- id_token
- access_token
{
"aud": [
"my_client"
],
"auth_time": 1647427485,
"your:custom:id-token-claim": "another value",
"your:second:id-token-claim": 2394123,
"iss": "http://ory.hydra.example/",
"sub": "foo@bar.com"
}
{
"active": true,
"scope": "openid offline",
"client_id": "my_client",
"sub": "foo@bar.com",
"aud": [],
"iss": "http://ory.hydra.example/",
"token_type": "Bearer",
"token_use": "access_token",
"ext": {
"your:custom:access-token-claim": "any value you like",
"your:second:access-token-claim": 124390123,
}
}
You cannot override the token subject.
Refresh token
If a webhook for refresh_token
grant type fails with a non-graceful result, the refresh flow will fail and the supplied
refresh_token
will remain unused.
Legacy webhook implementation (deprecated)
This mechanism is deprecated and no longer supported
There is an old version of the webhook feature built specifically for the refresh_token
grant type. We recommend using the
generic webhook feature because the old one will soon be deprecated.
Use the Ory CLI with following keys to enable this feature:
ory patch oauth2-config {project.id} \
--add '/oauth2/refresh_token_hook/url="https://my-example.app/token-refresh-hook"' \
--format yaml
Legacy webhook payload
The legacy webhook feature works the same way as the new one, but has a different payload that is sent to the webhook URL.
The refresh_token
hook endpoint must accept the following payload format:
{
"subject": "foo",
"client_id": "bar",
"session": {
"id_token": {
"id_token_claims": {
"jti": "jti",
"iss": "http://localhost:4444/",
"sub": "foo",
"aud": ["bar"],
"iat": 1234567,
"exp": 1234567,
"rat": 1234567,
"auth_time": 1234567,
"nonce": "",
"at_hash": "",
"acr": "1",
"amr": [],
"c_hash": "",
"ext": {}
},
"headers": {
"extra": {
"kid": "key-id"
}
},
"username": "username",
"subject": "foo",
"expires_at": 1234567
},
"extra": {},
"client_id": "bar",
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"kid": "key-id"
},
"requester": {
"client_id": "bar",
"granted_scopes": ["openid", "offline"],
"granted_audience": [],
"grant_types": ["refresh_token"]
},
"granted_scopes": ["openid", "offline"],
"granted_audience": []
}
If you enable both legacy and the new webhook features, both will be executed for the refresh_token
grant type. The results of
both webhooks will be applied onto the session. In case of conflict, result of the new webhook will take priority.