OAuth2 webhooks
The Hydra OAuth2 and OpenID Connect server comes with a mechanism that allows updating id_token
and access_token
when a
registered client sends a token request. The flow is realized by calling the defined token hook endpoint which returns updated
data.
If the data provided by the webhook is different from the data the client sends, the webhook overwrites the session data with a new set.
The hook is called before any other logic is executed. If the hook execution fails, the entire token flow fails.
Configuration
You can use hooks feature with all grant types.
Use the Ory CLI with following keys to enable this feature:
ory patch oauth2-config {project.id} \
--add '/oauth2/token_hook="https://my-example.app/token-hook"' \
--format yaml
Webhook payload
The token hook endpoint must accept the following payload format:
{
"session": {
"id_token": {
"id_token_claims": {
"jti": "",
"iss": "http://localhost:4444/",
"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": {}
}
}
session
represents the consent session, along with the data that was passed to the
Accept Consent Request in the id_token
field.
request
is the token request context.
Webhook authentication
The webhook supports API key authentication. You can configure whether the API key is sent as a cookie or in a header, and the cookie/header name:
ory patch oauth2-config {project.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"' `# or cookie` \
--add '/oauth2/token_hook/auth/config/name="X-API-Key"' \
--add '/oauth2/token_hook/auth/config/value="MY API KEY"' \
--format yaml
Requester payload
For jwt-bearer
grant type the assertion
value is also sent to the webhook URL.
Here's the format of the `request.payload:
{
"assertion": ["eyJhbGciOiJIUzI..."]
}
For authorization_code
and refresh_token
grant types, the request.payload
is always empty.
Webhook responses
To update the data, the webhook must return a 200 OK
response and the updated session data in the following format:
{
"session": {
"access_token": {
"foo": "bar"
},
"id_token": {
"bar": "baz"
}
}
}
Alternatively, you can choose not to update the session data by returning a 204 No Content
response.
The token subject is never overridden.
Updated tokens
The following examples show fragments of tokens issued after the webhook call:
- id_token
- access_token
{
"aud": [
"my_client"
],
"auth_time": 1647427485,
"bar": "baz",
"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": {
"foo": "bar"
}
}
Rejecting token claims update
To gracefully reject token contents update, the hook must return a 403 Forbidden
response. Any other response results in a
failure of the token update and, as a result, failure of the entire token flow.
Refresh token
If a webhook for refresh_token
grant type fails with a non-graceful result, the refresh flow will also fail and the supplied
refresh_token
will remain unused.
Legacy webhook implementation
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
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.