Skip to main content

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.

note

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:

Enable the generic token hook
ory patch oauth2-config {project.id} \
--add '/oauth2/token_hook/url="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": {}
}
}
note

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:

Add an API key to the token hook
ory patch oauth2-config {project.id} \
--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..."]
}
note

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.

note

The token subject is never overridden.

Updated tokens

The following examples show fragments of tokens issued after the webhook call:


{
"aud": [
"my_client"
],
"auth_time": 1647427485,
"bar": "baz",
"iss": "http://ory.hydra.example/",
"sub": "foo@bar.com"
}

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:

Enable the refresh token hook
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": []
}
note

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.