Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

Tokens & Sender Constraint

Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated

Token formats

TokenFormatDefault lifetime
Access tokenSigned JWT, typ: at+jwt (RFC 9068)issuer.access_token_ttl, 10m
ID tokenSigned JWTissuer.id_token_ttl, 10m
Refresh tokenOpaque, kept in the databaseissuer.refresh_token_ttl, 720h
Authorization codeOpaque, single useissuer.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 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) 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 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:

EventRefresh tokens retired
Logout (hosted sign-out, RP-initiated logout)Those issued in the session that ended
An administrator ends one sessionThose 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 userEvery 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 listsThe same, and every other session of the user ends
An administrator suspends or blocks the userEvery refresh token the account was given until then; every session ends
The user is deletedEvery 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

FeatureSpecificationNotes
Pushed authorization requestsRFC 9126POST /par returns a single-use request_uri. Set fapi.require_pushed_authorization_requests: true to require PAR for every client.
Signed request objectsRFC 9101A 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 requestsRFC 9396Not 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 indicatorsRFC 8707resource at /authorize and /token restricts the token's audience.
Issuer identificationRFC 9207iss 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. 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.