# Tokens & Sender Constraint

> Access, ID and refresh tokens in nauthera-server and their claims; DPoP sender-constrained tokens; PAR, JAR and resource indicators; signing keys and FAPI 2.0.

## Token formats

| Token | Format | Default lifetime |
|---|---|---|
| Access token | Signed JWT, `typ: at+jwt` (RFC 9068) | `issuer.access_token_ttl`, 10m |
| ID token | Signed JWT | `issuer.id_token_ttl`, 10m |
| Refresh token | Opaque, kept in the database | `issuer.refresh_token_ttl`, 720h |
| Authorization code | Opaque, single use | `issuer.auth_code_ttl`, 60s |

A client registered through the AdminService can set its own access and ID token lifetimes
(`access_token_ttl_seconds`, `id_token_ttl_seconds`; `0` means the issuer default), and
its own refresh lifetime (`refresh_token_ttl_seconds`), at most `issuer.refresh_token_ttl`,
which is also its default. Dynamically registered clients always use the issuer
defaults. Every token the server verifies must carry an expiry, and the server
enforces the token's audience on its own resources.

### Refresh tokens

A refresh token is issued only when the client may refresh **and** the user granted
`offline_access`. Refresh tokens **rotate on every use**. The refresh token's binding to
its client is checked before the token is consumed. Narrowing the scope on a refresh
narrows that token, not the underlying grant. Each new refresh token lives for the
client's refresh lifetime (by default `issuer.refresh_token_ttl`), so a family that keeps
refreshing has no end; an absolute lifetime for a family is planned (#310).

Every token minted from one authorization, the refresh chain and the access tokens
beside it, belongs to one **grant family**, which a user's access token names in its
private `fam` claim. Presenting an already-used refresh token, or redeeming an
authorization code a second time, revokes that family: the live refresh token descended
from it stops rotating, and its access tokens are refused at the server's own
endpoints (UserInfo, introspection, the admin API and client registration) even before
they expire. A resource server that validates access tokens locally does not see this.
Telling a client's retry of a refresh apart from a stolen token's replay is planned
(#1387).

Refresh tokens are kept in the **database**, under a SHA-256 digest of the token, with
the tombstones that recognize a reused one and the markers that revoke a family, a
session or an account. A Redis outage or restart therefore neither signs users out
nor undoes a revocation. For one refresh-token lifetime after an upgrade from a build
that kept them in Redis, the server still reads the tokens and markers that build left
there, so nobody is signed out by the upgrade and no revocation is lost. Authorization
codes and their tombstones stay in Redis.

### Account status

The user's account is read on **every** grant that issues tokens for a user: the
authorization code, a refresh and the device code. A
[suspended or blocked](https://www.nauthera.io/docs/v1/login-experience#account-status) account is refused
with the grant's ordinary `invalid_grant`, the answer it gives a spent or unknown
credential, so a code or token holder does not learn the account's status.

Suspending or blocking an account (`POST …/users/{id}/suspend` or `/block`, see
[Admin API & Users](https://www.nauthera.io/docs/v1/user-management#account-status)) takes effect at once:
every session ends, every refresh grant the account was given is retired, device-code
grants included, and relying parties are told over back-channel logout. A code or grant
still in flight is refused with that ordinary `invalid_grant`. After reactivation the user
signs in again. Access tokens already issued stay valid until they expire.

### Profile claims

The `email` scope releases `email` and `email_verified`; `profile` releases `name` and
`preferred_username`. They appear in the ID token, the access token and UserInfo alike.
`email_verified` is `true` once the user followed a
[verification link](https://www.nauthera.io/docs/v1/login-experience#password-reset-and-address-verification)
sent to their current address, or reset their password from a link sent there, or an
administrator vouched for the address, and `false` otherwise. Changing the address clears
it, unless the administrator vouches for the new one. It is released only together with a
non-empty `email`.

### Revocation and introspection

`/revoke` (RFC 7009) revokes refresh tokens only, and only the one token named, not the
grant behind it. A revoked access token stays valid until it expires (#91).
`/introspect` (RFC 7662) reports an access token's state, including `cnf` and
`token_type` for DPoP-bound tokens; a refresh token is reported inactive (#93).
Introspection is **caller-bound**: only the confidential client a token was issued to
can introspect it. Public clients are refused, and a token belonging to another client is
reported inactive. A separate resource server therefore cannot use `/introspect`; it
should validate the JWT locally against the JWKS.

Ending a user's access also retires refresh tokens:

| Event | Refresh tokens retired |
|---|---|
| Logout (hosted sign-out, RP-initiated logout) | Those issued in the session that ended |
| An administrator ends one session | Those issued in that session |
| A password reset, by the user from an emailed link or by an administrator; an administrator ending every session of the user | Every refresh token the account was given until then, from any session, client or flow, device-code grants and grants that outlived their session included |
| A second-factor change that [Multi-Factor Authentication](https://www.nauthera.io/docs/v1/mfa#changing-a-factor) lists | The same, and every other session of the user ends |
| An administrator suspends or blocks the user | Every refresh token the account was given until then; every session ends |
| The user is deleted | Every refresh token of the account |

A grant from a sign-in after the event keeps working. Access tokens already issued run to
their expiry in every case. Revoking every grant of a client on demand is planned
(#1389).

## DPoP: sender-constrained tokens

DPoP (RFC 9449) binds a token to a key the client holds, so a stolen token is useless
without that key.

- **Proofs** are verified once per request: signature, `htm`, `htu`, the `iat` window,
  the nonce, `ath` at a protected resource, and a replay cache for `jti`.
  Accepted proof algorithms are ES256, PS256/384/512 and RS256/384/512.
- **Nonces are always required.** A proof without a current nonce is refused with
  `use_dpop_nonce`, and a `DPoP-Nonce` header carries the nonce to retry with: `400` at
  `/token`, `401` with a `WWW-Authenticate: DPoP` challenge at a protected resource. A
  nonce stays valid for five minutes, and every successful token response to a request
  with a proof carries a fresh one. A nonce belongs to one organization: a nonce from one
  organization's endpoints is refused at another's, and the refusal carries a new one.
- **Binding.** If the token request carries a proof, the issued access token carries
  `cnf.jkt` (the key's thumbprint). This works for every grant: authorization code,
  refresh, client credentials and device code.
- **Early binding.** `dpop_jkt` on `/authorize` and `/par` binds the authorization code to
  the key before the code exists. On `/device_authorization` it binds the device code: a
  poll with that device code must carry a proof for that key. A `DPoP` *header* sent to
  `/par` is ignored (#133); use the `dpop_jkt` parameter there. The key requested at
  `/authorize` is carried through sign-in and consent.
- **Enforcement.** A bound token presented without a matching proof is refused at
  UserInfo, the admin API and client registration (RFC 9449 §7.1). It must also be sent
  with the `DPoP` scheme; under `Bearer` it is refused with `401`, even with a valid proof.

A client cannot yet require that all of its own tokens be sender-constrained
(`dpop_bound_access_tokens`, #138). The remaining DPoP work is tracked in #34.

## Authorization request integrity

| Feature | Specification | Notes |
|---|---|---|
| Pushed authorization requests | RFC 9126 | `POST /par` returns a single-use `request_uri`. Set `fapi.require_pushed_authorization_requests: true` to require PAR for every client. |
| Signed request objects | RFC 9101 | A `request` passed by value, signed with ES256 or RS256 by a key at the client's `jwks_uri`. `none` and HMAC are refused. `request_uri` accepts PAR references only. |
| Rich authorization requests | RFC 9396 | **Not usable yet.** `authorization_details` is validated against the detail types a client is registered for, but no registration path can set those types, so every request carrying it is refused. Tracked in #36 and #59. |
| Resource indicators | RFC 8707 | `resource` at `/authorize` and `/token` restricts the token's audience. |
| Issuer identification | RFC 9207 | `iss` is returned on the authorization response. |

## Signing keys

The server signs with **ES256** (EC P-256) and **RS256**. EdDSA and PS256 are not offered
for signing. Configure keys with `keys.provider`:

- `memory` generates keys at startup. It is for development only and is refused for an
  issuer that is not loopback.
- `file` reads PEM keys from `keys.signing_key_path` (EC) and
  `keys.rsa_signing_key_path` (RSA). If only the EC key is configured, the deployment is
  ES256-only and discovery says so.

Each organization other than the main one has its own signing keys. They are stored in
the database, encrypted under `keys.encryption_key`.

**Rotation is manual.** To rotate the main organization's keys, publish the new public keys
first, through `keys.verification_key_paths` and `keys.rsa_verification_key_paths`, while
the current keys still sign. Restart every replica, then wait at least eleven minutes: the
server's cached copy of the JWKS can take up to 330 seconds to list the new keys, and a
client may keep its copy for 300 seconds more. Then sign with the new keys, publish the
previous public keys the same way, and restart again. Once the longest access and ID
token lifetime has passed since that restart, remove the previous public keys and restart
again. The full procedure is in
[Keys & Secrets](https://www.nauthera.io/docs/v1/keys-and-secrets#rotating-the-main-orgs-signing-key).
Organizations other than the main one cannot rotate their keys today. Rotating them
without a restart is tracked in #300.

## FAPI 2.0 mode

An organization with `fapi_mode` on, set at creation or later, applies part of the FAPI 2.0
Security Profile:

- pushed authorization requests are required,
- PKCE with S256 is required,
- signing and request-object processing use ES256 only, and
- the authorization code lifetime is capped at 60 seconds.

It does **not** yet refuse shared-secret client authentication (#33) or require
sender-constrained tokens (#54). The full profile is tracked in #35, and naming the
switch after what it currently enforces is tracked in #57.
