Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/content/deployment/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,11 @@ OAuth 2.0 and OpenID Connect settings.
| `oauth.refresh_token.renew_on_grant` | `false` | If `true`, issues a new refresh token on each access token grant |
| `oauth.refresh_token.revoke_previous_on_renew` | `true` | If `true`, revokes the consumed refresh token after a successful renewal. Applies when `renew_on_grant` is enabled |
| `oauth.refresh_token.validity_period` | `86400` | Refresh token validity period in seconds (24 hours) |
| `oauth.token_revocation.enabled` | `true` | If `true`, enables token revocation on the authorization server: the RFC 7009 endpoint (`/oauth2/revoke`), the implicit revocation triggers, and the check that rejects revoked tokens during introspection, the refresh grant, and token exchange. Setting it to `false` turns off all of them, not only the endpoint. See [Token Revocation](../../guides/protocols/oauth-oidc/token-revocation) |
| `oauth.revocation.token_family.on_explicit_revoke` | `true` | If `true`, revoking a token through `/oauth2/revoke` also revokes every other token of the same authorization grant |
| `oauth.revocation.token_family.on_refresh_replay` | `true` | If `true`, presenting a refresh token that has already been rotated out revokes every token of that grant. Requires `oauth.refresh_token.renew_on_grant`, since a refresh token can only be replayed once it has been rotated |
| `oauth.revocation.token_family.on_code_replay` | `true` | If `true`, redeeming an authorization code a second time revokes every token issued from that code |
| `oauth.token_exchange.token_family` | `none` | Whether an exchanged token joins the subject token's authorization grant. `none` issues an independent token; `inherit` copies the subject token's token family, so revoking the grant also revokes the exchanged token |
| `oauth.authorization_code.validity_period` | `600` | Authorization code validity period in seconds (10 minutes) |
| `oauth.authorization_request.validity_period` | `3600` | How long the authorization request context stays valid while the user completes the login flow, in seconds (60 minutes). A non-positive value falls back to the default |
| `oauth.dcr.enabled` | `true` | If `true`, enables the Dynamic Client Registration endpoint |
Expand Down Expand Up @@ -1010,7 +1015,9 @@ Each flow type has two fields:
| `<type>.defaultHandle` | Handle of the flow to use when the application and the organization unit do not pin a specific flow. Empty string means no server default, and the flow type is treated as not configured at the server level. |
| `<type>.expirySeconds` | How long (in seconds) a started flow context remains valid before it expires. Must be a positive integer. |

Supported flow types are `authFlow`, `registrationFlow`, `userOnboardingFlow`, `recoveryFlow`, and `signOutFlow`.
Supported flow types are `authFlow`, `registrationFlow`, `userOnboardingFlow`, `recoveryFlow`, `signOutFlow`, and `userDeletionFlow`.

`userDeletionFlow` is resolved differently from the others: it names an administration flow that runs when a user is deleted from the Console, rather than a flow an inbound request starts. Its shipped default revokes the user's tokens and terminates their sessions before the record is removed. Deleting a user through the `DELETE /users/{id}` API removes the record only and does not run this flow. See [Token Revocation](../../guides/protocols/oauth-oidc/token-revocation#user-deletion).

**Resolution order:** When an inbound request starts a flow, the effective flow is resolved as follows:

Expand Down Expand Up @@ -1075,6 +1082,9 @@ Controls server-wide security behavior that is not specific to any single authen
| `server.security.jwks_cache_ttl` | `300` | JWKS cache TTL in seconds. Applies to every JWKS consumer in the server (trusted issuer validation, federated OIDC authenticators such as Google, and so on). Fetched signing keys are reused from the in-process cache for this duration before being re-fetched. Plan external-server key rotations with at least this much overlap. Set to `0` to disable caching |
| `server.security.system_permission_prefix` | `""` (empty) | Prefix for system permission strings used in API authorization. When empty, permissions use their base names (for example, `system`). When set, the prefix is prepended to every system permission (for example, `mgmt:system`). If you set a prefix, update the Console scopes to match. Changes require a server restart |
| `server.security.direct_auth_secret` | `""` (empty) | Secret that gates the Direct API authentication endpoints (`/auth/**` and `/register/passkey/**`) and protected AuthZEN access endpoints (`/access/**`). AuthZEN discovery (`/.well-known/authzen-configuration`) remains public. The protected endpoints are **secure by default**. While this is empty they are blocked with `401`. When set, callers must send the value in the `Direct-Auth-Secret` header; a missing or incorrect value is rejected with `401`. See [Integration Models](../../key-concepts/authentication/integration-models#direct-api) |
| `server.security.token_revocation.enabled` | `true` | If `true`, revoked tokens are rejected on requests to protected APIs. See [Token Revocation](../../guides/protocols/oauth-oidc/token-revocation) |
| `server.security.token_revocation.source` | `db` | Where the revocation snapshot is read from. `db` is the only supported value; any other value fails startup validation |
| `server.security.token_revocation.sync_interval_seconds` | `60` | How often the in-memory revocation snapshot refreshes, in seconds. This sets the normal propagation delay before a revoked token stops being accepted on protected API requests. Token introspection is unaffected and always reads current state |

:::tip
If you set a custom `system_permission_prefix`, update the Console scopes to match the new permission strings and restart the server.
Expand Down
146 changes: 146 additions & 0 deletions docs/content/guides/protocols/oauth-oidc/token-revocation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: Token Revocation
docType: reference
sidebar_position: 2
description: RFC 7009 token revocation in {{ProductName}}, from revoking a single token to invalidating every token of a grant or a user, and where each is enforced.
---

# Token Revocation

**Token Revocation** ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)) lets a client tell <ProductName /> that a token should stop working before it expires. <ProductName /> extends this with two broader scopes, so an operational event such as a sign-out or a user deletion can invalidate many tokens at once.

Access tokens are self-contained JWTs, so a resource server that validates them locally has no way to know a token was revoked. <ProductName /> solves this with a denylist that both the authorization server and resource servers consult. The three scopes below differ only in what goes on that list.

## The Three Scopes

| Scope | What it invalidates | Typical trigger |
|---|---|---|
| **Single token** | One token, by its `jti` | A client calls `/oauth2/revoke`; a refresh token is rotated |
| **Grant** | Every token of one authorization grant, by its `tfid` | Sign-out, refresh-token replay, authorization-code replay |
| **User** | Every token belonging to one user, by its `sub` | A user is deleted |

A token is rejected if **any** of these matches.

## Revoke a Single Token

<details>
<summary>How <ProductName /> Implements It</summary>

| Aspect | Behavior |
|---|---|
| Endpoint | `POST /oauth2/revoke` |
| Authentication | Required, same client authentication method as the token endpoint (see [Client Authentication Methods](../client-authentication-methods)) |
| `token` parameter | Required. The access token or refresh token to revoke |
| `token_type_hint` parameter | Accepted but not used internally: the server determines the token type itself |
| Success response | HTTP 200 with an empty body, as RFC 7009 §2.2 requires |
| Unknown or already-expired token | HTTP 200. The endpoint never reveals whether a token existed |
| Token issued to another client | HTTP 400 with `invalid_grant` |
| Tokens supported | Access tokens and refresh tokens |
| Signature | Verified before anything is recorded, so a token <ProductName /> did not issue cannot be added to the denylist |
| Expired tokens | Still revocable. Expiry is ignored on this endpoint, so an expired token can be presented to revoke the rest of its grant |

</details>

```bash
curl -X POST https://{{productSlug}}.example.com/oauth2/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "token=$ACCESS_TOKEN"
```

A successful revocation returns `200 OK` with no body.

### Ownership

RFC 7009 §2.1 requires the server to verify that the token was issued to the authenticated client. <ProductName /> enforces this for both token types: access tokens are matched on their `client_id` claim, and refresh tokens on their `sub` claim, which carries the owning client. A client attempting to revoke another client's token receives `invalid_grant`.

## Grant-Scoped Revocation

Revoking one token of a grant usually should not leave its siblings working. Five refreshes produce a chain of access and refresh tokens that all descend from the same authorization, and revoking the newest refresh token alone would leave live access tokens behind.

<ProductName /> groups these into a **token family**, identified by a `tfid` claim carried on every token of the grant. By default, revoking any token of a family revokes the family in both directions: revoking a refresh token invalidates the access tokens issued alongside it, and revoking an access token invalidates the refresh token that would mint replacements. [What Revokes a Family](#what-revokes-a-family) below lists the setting that controls this, along with the other triggers.

### Where `tfid` Comes From

| Flow | Behavior |
|---|---|
| Authorization code with SSO | Minted when the session is established and stored against the session participant, which is what makes sign-out revocation possible |
| Authorization code without SSO | Minted when the authorization code is created, so the grant is still revocable |
| Refresh | Copied to the new tokens, so a family survives rotation |
| Token exchange | Controlled by `oauth.token_exchange.token_family`. `none` (the default) issues an independent token; `inherit` copies the subject token's family |
| Client credentials, CIBA | No `tfid`. There is no user grant to group, so family-scoped revocation does not apply |

### What Revokes a Family

| Trigger | Setting | Default |
|---|---|---|
| A client revokes a token that carries a `tfid` | `oauth.revocation.token_family.on_explicit_revoke` | `true` |
| A rotated-out refresh token is presented again (replay) | `oauth.revocation.token_family.on_refresh_replay` | `true` |
| An authorization code is redeemed twice (replay) | `oauth.revocation.token_family.on_code_replay` | `true` |
| The user signs out through RP-Initiated Logout | Always on | Not configurable |

:::note
Refresh-token replay detection depends on rotation, since a token can only be replayed once it has been rotated out. Enable `oauth.refresh_token.renew_on_grant` to use it.
:::

Session **sign-out** revokes the family. Session **expiry** does not: a session that simply times out leaves its tokens valid until they expire on their own.

## User-Scoped Revocation

Some events invalidate tokens that no single client is holding, and that span more than one grant. Deleting a user is the clearest case: their tokens may be spread across several applications and devices, and nobody presents them to be revoked.

For these, <ProductName /> revokes by **subject**: every token issued to that user is rejected, across every application and device, without anyone having to present or enumerate them. Tokens already carry `sub`, so no additional claim is needed.

### User Deletion

<ProductName /> ships an administration flow that runs when a user is deleted from the Console. It checks the caller's permission, revokes the user's tokens by subject, terminates their sessions, and then removes the record. `flow.userDeletionFlow.defaultHandle` names the flow to run. Pointing it at a different administration flow replaces the shipped one.

:::warning
Deleting a user through the `DELETE /users/{id}` API removes the record only. It does not run the deletion flow, so the user's tokens stay valid until they expire. To revoke tokens as part of deletion, delete from the Console, or run the deletion flow directly from automation.
:::

## Where Revocation Is Enforced

Revocation is checked at two independent points, and they do not become consistent at the same speed.

| Enforcement point | What it covers | Freshness |
|---|---|---|
| **Authorization server** | Introspection, the refresh grant, token exchange, and internal validation | **Immediate.** Reads the denylist directly |
| **Resource server** | Requests to <ProductName />'s own protected APIs | **Eventually consistent.** Serves an in-memory snapshot refreshed on an interval |

:::note
A revoked token normally stops being accepted by a resource server within `server.security.token_revocation.sync_interval_seconds` (60 seconds by default). For an immediate answer, call [Token Introspection](../token-introspection), which reads current state. Lower the sync interval to shorten the delay, at the cost of more frequent database reads.
:::

Authorization-server enforcement **fails closed**. If the denylist cannot be read, token validation fails rather than assuming the token is good. Resource-server enforcement keeps serving its last good snapshot across a transient refresh failure.

Resource-server enforcement covers services that share this deployment's database. An external resource server with no access to it should use [Token Introspection](../token-introspection) instead.

## Configuration

Revocation behavior on the authorization server, part of [OAuth Configuration](../../../../deployment/configuration#oauth-configuration):

| Setting | Default | Description |
|---|---|---|
| `oauth.token_revocation.enabled` | `true` | Enables revocation on the authorization server: the `/oauth2/revoke` endpoint, the implicit triggers below, and the check that rejects revoked tokens during introspection, the refresh grant, and token exchange. Setting it to `false` turns off all of them, not just the endpoint |
| `oauth.revocation.token_family.on_explicit_revoke` | `true` | Revoking a token also revokes its token family |
| `oauth.revocation.token_family.on_refresh_replay` | `true` | A replayed refresh token revokes its token family. Requires `oauth.refresh_token.renew_on_grant` |
| `oauth.revocation.token_family.on_code_replay` | `true` | A replayed authorization code revokes the token family issued from it |
| `oauth.token_exchange.token_family` | `none` | `none` issues an independent token; `inherit` keeps the subject token's family |
| `oauth.refresh_token.renew_on_grant` | `false` | Issues a new refresh token on each grant. Required for rotation and replay detection |
| `oauth.refresh_token.revoke_previous_on_renew` | `true` | Revokes the consumed refresh token after rotation, making refresh tokens single-use |

Revocation enforcement on resource-server requests, part of [Security Configuration](../../../../deployment/configuration#security-configuration):

| Setting | Default | Description |
|---|---|---|
| `server.security.token_revocation.enabled` | `true` | Enforces revocation on requests to protected APIs |
| `server.security.token_revocation.source` | `db` | Where the revocation snapshot is read from. `db` is the only supported value |
| `server.security.token_revocation.sync_interval_seconds` | `60` | How often the snapshot refreshes, which sets the normal propagation delay before a revoked token stops being accepted |

## Related Guides

- [Token Introspection](../token-introspection), for checking revocation state immediately
- [Refresh Token](../refresh-token), for rotation and its relationship to replay detection
- [RP-Initiated Logout](../rp-initiated-logout), which revokes the session's token family
- [Token Exchange](../token-exchange), for how exchanged tokens inherit or escape a family
- [Configuration](../../../../deployment/configuration), for where these settings live
5 changes: 5 additions & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,11 @@ const sidebars: SidebarsConfig = {
id: 'guides/protocols/oauth-oidc/token-introspection',
label: 'Token Introspection',
},
{
type: 'doc',
id: 'guides/protocols/oauth-oidc/token-revocation',
label: 'Token Revocation',
},
],
},
{
Expand Down
Loading