# System Design: [Identity provider name]

> Reference design for an identity provider (IdP) that offers single
> sign-on to first-party and third-party applications as an OAuth 2.0
> authorization server and OpenID Connect issuer. It covers the
> authorization code flow with PKCE, token formats and lifetimes, refresh
> token rotation with reuse detection, signing key rotation and the JWKS
> endpoint, browser sessions and logout, multi-factor authentication with
> passkeys (WebAuthn), account recovery and multi-tenant issuers. It is
> based on OpenID Connect Core 1.0 incorporating errata set 2, RFC 6749,
> RFC 7636 (PKCE), RFC 9700 (OAuth 2.0 Security Best Current Practice,
> January 2025), NIST SP 800-63-4 (July 2025) and W3C Web Authentication
> Level 3. It follows the 12-section structure of the System Design
> Document template. Replace every `[bracketed]` value and every row marked
> *Example*.

<!--
Guidance: How to use this template
- Start with the client inventory (section 2.1 and 5.4): which apps are
  public clients (browser, mobile) and which are confidential (servers).
  The grant types, token lifetimes and sender-constraint choices follow
  from it.
- Write the token lifecycle table (7.1) before the API section. Most IdP
  incidents are lifetime and revocation mistakes, not crypto mistakes.
- Treat account recovery (7.4) as an authentication path with the same
  assurance as login. A strong login with a weak recovery is a weak login.
- This document designs the IdP service itself. For the security review of
  a product that uses it, see Security Design Review Template
  (`security/security-review`); for the threat analysis, Threat Model
  Template (`security/threat-model`); for the enterprise-wide identity
  pillar, Zero Trust Architecture Plan Template
  (`security/zero-trust-architecture`). Fine-grained permissions belong in
  Zanzibar-Style Authorization Service System Design Template
  (`system-design/design-authorization-service`).
-->

## Document control

<!-- Guidance: Identity is a tier-0 dependency: name the security approver and the incident owner, not only the engineering lead. -->

| Field | Value |
|---|---|
| Document ID | [SDD-IDP-001] |
| System / feature | [Customer and workforce single sign-on] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Confidential] |
| Target assurance | [*Example:* NIST SP 800-63B-4 AAL2 for customers, AAL3 for administrators] |
| Related | [Threat model], [Authorization service design], [ADR: token format] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Security architect | [@name] | [Decision] | YYYY-MM-DD |
| Privacy / data protection | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Product (sign-in experience) | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Refresh token rotation with family revocation added |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* Passkeys as primary factor; SMS removed from recovery |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page a busy reviewer can read alone. State the assurance level you target and who the users are; everything else depends on them. -->

### 1.1 Problem statement

[*Example:* Six products each run their own login, password storage and
"remember me" cookies. Users hold several passwords for one company, a
credential-stuffing attack last quarter succeeded against the oldest
product, and enterprise customers ask for SSO with their own identity
provider. Mobile apps still use the resource owner password grant, which
current OAuth security guidance forbids.]

### 1.2 Proposed solution

[*Example:* One identity provider per region serves every product as an
OpenID Connect issuer. Applications redirect to it with the authorization
code flow and PKCE; it authenticates the user with a passkey, or a
password plus a second factor, keeps a browser session, and issues a short
ID token, a 10-minute JWT access token and a rotating refresh token. Keys
are rotated on a schedule and published at the JWKS endpoint ahead of use.
Enterprise tenants get their own issuer and can federate to their own IdP.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Access token format? | JWT per RFC 9068 for first-party APIs; opaque with introspection for third parties | 7.1, 10 |
| Q-02 | *Example:* Refresh token protection for public clients? | Rotation with reuse detection; DPoP where the client supports it | 7.1 |
| Q-03 | *Example:* Primary factor? | Passkeys first; password plus second factor as fallback | 7.3 |
| Q-04 | *Example:* Multi-tenant issuer model? | One issuer URL per enterprise tenant; shared issuer for consumers | 7.5 |
| Q-05 | *Example:* Signing key custody? | Keys generated and wrapped by KMS, held in memory of the token service | 7.2 |
| [Q-nn] | [Question] | [Option] | [Section] |

### 1.4 45-minute interview path

<!-- Guidance: In an interview, cover the minutes below and pick one deep dive. Name the others so the interviewer knows you see them. -->

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Clarify: consumers or workforce, first-party only or third-party apps, MFA, enterprise federation | Core and below-the-line lists (2.1, 2.3) |
| 5 to 10 | Estimate logins/s, refreshes/s, sessions, hashing CPU | Numbers with consequences (3.7) |
| 10 to 18 | Authorization code flow with PKCE as a sequence diagram | Diagram from 7.0 |
| 18 to 24 | Endpoints and token types: ID, access, refresh | Token lifecycle table (7.1) |
| 24 to 30 | High-level design: login UI, token service, session store, key service | One diagram (6) |
| 30 to 40 | One deep dive: refresh rotation (7.1) or key rotation (7.2) | Options, choice, reason |
| 40 to 45 | Failure modes: IdP down means nobody logs in; key compromise | Two rows of 9.1 |

## 2. Requirements

<!-- Guidance: Separate core requirements from those below the line. Pitfall: promising every protocol (SAML, LDAP, CAS, device flow) in the first release. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* OpenID Connect authorization code flow with PKCE (S256) for all clients | Must |
| FR-02 | *Example:* Client credentials grant for service-to-service calls | Must |
| FR-03 | *Example:* Discovery document and JWKS endpoint | Must |
| FR-04 | *Example:* Refresh tokens with rotation and reuse detection | Must |
| FR-05 | *Example:* Passkeys (WebAuthn) as primary factor; password plus TOTP as fallback | Must |
| FR-06 | *Example:* Browser SSO session across first-party apps; RP-initiated and back-channel logout | Must |
| FR-07 | *Example:* Token revocation (RFC 7009) and introspection (RFC 7662) | Must |
| FR-08 | *Example:* Account recovery without weakening the assurance level | Must |
| FR-09 | *Example:* Enterprise tenants federate to their own OIDC or SAML IdP | Should |
| FR-10 | *Example:* Step-up authentication requested by `acr_values` | Should |
| FR-11 | *Example:* Consent screen and stored grants for third-party clients | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Availability of token endpoint | *Example:* 99.99% monthly | Successful token responses / valid requests |
| NFR-02 | Token endpoint latency | *Example:* p99 at or below 150 ms (excluding password hashing) | Server timing |
| NFR-03 | Login page latency | *Example:* p95 at or below 800 ms end to end | Real user monitoring |
| NFR-04 | Assurance | *Example:* AAL2 for customers; AAL3 for administrators | Authenticator inventory review |
| NFR-05 | Revocation | *Example:* Refresh revocation effective immediately; access token exposure at most its lifetime | Test |
| NFR-06 | Key rotation | *Example:* No failed validations during scheduled rotation | Rotation drill |
| NFR-07 | Credential storage | *Example:* Passwords hashed with a memory-hard function; no reversible secrets | Security review |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Below the line (out of scope)

- [*Example:* implicit flow and resource owner password credentials grant (not offered; RFC 9700 §2.1.2 and §2.4)]
- [*Example:* device authorization grant for TVs (phase 2)]
- [*Example:* identity proofing of real-world identity (handled by a proofing vendor if ever needed)]
- [*Example:* fine-grained permissions (the authorization service decides; the IdP only authenticates)]
- [*Example:* SCIM user provisioning from enterprise directories (phase 2)]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Every number must end in a consequence. For an IdP, the expensive operations are password hashing and refresh rotation writes, not token signing. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Registered accounts | $A$ | *Example:* 50 million | User database |
| Daily active users | $D$ | *Example:* 5 million | Product analytics |
| Interactive logins per active user per day | $l$ | *Example:* 2 | Session logs |
| Peak-to-average factor for logins | $p_l$ | *Example:* 5 | Morning peak |
| Active hours per active user per day | $u$ | *Example:* 4 | Session logs |
| Access token lifetime | $t_a$ | *Example:* 10 min | 7.1 |
| Peak-to-average factor for refreshes | $p_r$ | *Example:* 3 | Traffic shape |
| Share of logins verifying a password | $q$ | *Example:* 0.6 | Factor mix |
| CPU time per password verification | $c$ | *Example:* 50 ms | Benchmark of the chosen hash parameters |

### 3.2 Interactive logins

$$
L_{peak} = \frac{D \times l}{86\,400} \times p_l = \frac{5 \times 10^{6} \times 2}{86\,400} \times 5 \approx 116 \times 5 = 580 \ \text{logins/s}
$$

Consequence: the login front end is small; capacity planning focuses on
the hashing pool below.

### 3.3 Password hashing CPU

$$
H = L_{peak} \times q \times c = 580 \times 0.6 \times 0.05 \ \text{s} \approx 17.4 \ \text{cores}
$$

Consequence: with 40% headroom, about 24 cores at peak. A credential
stuffing attack at 10 times the normal rate would need 240 cores, so rate
limiting and bot detection run before the hash, not after it, and the
hashing pool is isolated so an attack cannot starve token refreshes.

### 3.4 Token refreshes

$$
R_{peak} = \frac{D \times u \times \frac{60}{t_a}}{86\,400} \times p_r = \frac{5 \times 10^{6} \times 4 \times 6}{86\,400} \times 3 \approx 1\,389 \times 3 \approx 4\,170 \ \text{refreshes/s}
$$

Consequence: each refresh with rotation is a conditional write (mark the
old token used, insert the new one). Over 4 000 writes/s needs a store
that handles conditional writes at that rate per region, so refresh tokens
get their own table keyed by token hash (5.3).

### 3.5 State size

| Data | Count | Bytes each | Total | Consequence |
|---|---|---|---|---|
| Browser sessions | *Example:* 10 million | 1 KB | 10 GB | Fits in an in-memory store with replicas |
| Refresh token families | *Example:* 20 million | 400 B | 8 GB | Single table, TTL-based expiry |
| Passkey credentials | *Example:* 30 million | 600 B | 18 GB | Ordinary indexed table |
| Audit events | *Example:* 50 million/day | 500 B | 25 GB/day | Separate append-only pipeline |

### 3.6 Signing and key distribution

Peak signatures are about $580 + 4\,170 \approx 4\,750$/s (one ID or access
token set per login or refresh, counted once). Elliptic-curve signatures
take well under a millisecond each, so a handful of cores suffice.
Consequence: sign in the token service process with keys unwrapped from
KMS at start-up, rather than calling a remote KMS per token, which would
add latency and hit KMS rate limits. JWKS fetches by resource servers are
cached for minutes and served from a CDN.

### 3.7 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Logins | ~580/s peak | Small login tier |
| Password hashing | ~24 cores peak | Isolated hashing pool; rate limit before hashing |
| Refreshes | ~4 170/s peak | Dedicated refresh store with conditional writes |
| Sessions | ~10 GB | In-memory session store per region |
| Signatures | ~4 750/s | In-process signing, keys wrapped by KMS |

## 4. API design

<!-- Guidance: Use the standard endpoint and parameter names. Custom endpoints break off-the-shelf client libraries. Pitfall: accepting redirect URIs by prefix or wildcard. -->

### 4.1 Endpoints

| Endpoint | Standard | Purpose |
|---|---|---|
| `GET /.well-known/openid-configuration` | OpenID Connect Discovery 1.0; RFC 8414 | Metadata: endpoints, supported algorithms, grant types |
| `GET /jwks.json` | RFC 7517 | Public signing keys |
| `GET /authorize` | RFC 6749 §3.1; OIDC Core §3.1.2 | Start authentication; returns code to redirect URI |
| `POST /par` | RFC 9126 | Pushed authorization request for confidential clients |
| `POST /token` | RFC 6749 §3.2 | Exchange code, refresh, client credentials |
| `GET /userinfo` | OIDC Core §5.3 | Claims about the user |
| `POST /revoke` | RFC 7009 | Revoke refresh or access token |
| `POST /introspect` | RFC 7662 | Token state for resource servers using opaque tokens |
| `GET /logout` | OIDC RP-Initiated Logout 1.0 | End session and redirect |
| Back-channel logout token to client | OIDC Back-Channel Logout 1.0 | Notify clients that a session ended |

Grant types offered: `authorization_code` (PKCE required, S256 only),
`refresh_token`, `client_credentials`. Not offered: implicit and resource
owner password credentials, per RFC 9700 §2.1.2 and §2.4. Redirect URIs
are compared by exact string match (RFC 9700 §2.1), with the loopback port
exception for native apps.

### 4.2 Request and response examples

The authorization request carries a PKCE challenge, `state` for CSRF
protection and `nonce` to bind the ID token to the browser session.

```http
GET /authorize?response_type=code&client_id=web-app
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20profile%20email&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: id.example.com
```

```http
POST /token HTTP/1.1
Host: id.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&client_id=web-app
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYtMTAifQ...",
 "token_type": "Bearer", "expires_in": 600,
 "refresh_token": "rt_8xLOxBtZp8", "id_token": "eyJhbGciOiJFUzI1NiJ9..."}
```

*Example* ID token payload (claims defined in OIDC Core §2):

```json
{"iss": "https://id.example.com", "sub": "u_24400320", "aud": "web-app",
 "exp": 1792666800, "iat": 1792666500, "auth_time": 1792666490,
 "nonce": "n-0S6_WzA2Mj", "acr": "urn:example:aal2", "amr": ["hwk", "user"]}
```

### 4.3 Errors

Token endpoint errors use the codes from RFC 6749 §5.2 (`invalid_grant`,
`invalid_client`, `unauthorized_client`, `unsupported_grant_type`,
`invalid_scope`). Return `invalid_grant` for a reused refresh token and
revoke its family (7.1); never reveal whether a code or token existed.

## 5. Data model

<!-- Guidance: Show the entities, then the access patterns. Never store a bearer secret in a form that can be replayed if the table leaks: store hashes of codes and refresh tokens. -->

### 5.1 Entities

Accounts own credentials and sessions; clients own grants; signing keys
are global per issuer.

```mermaid
erDiagram
  TENANT ||--o{ ACCOUNT : contains
  TENANT ||--o{ CLIENT : registers
  ACCOUNT ||--o{ CREDENTIAL : has
  ACCOUNT ||--o{ SESSION : opens
  SESSION ||--o{ REFRESH_FAMILY : issues
  CLIENT ||--o{ REFRESH_FAMILY : "belongs to"
  ACCOUNT ||--o{ CONSENT : grants
  TENANT ||--o{ SIGNING_KEY : uses
  TENANT {
    string tenant_id PK
    string issuer_url
  }
  ACCOUNT {
    string account_id PK
    string tenant_id FK
    string email
    string status
  }
  CREDENTIAL {
    string credential_id PK
    string account_id FK
    string type
    bytes public_key_or_hash
    int sign_count
    bool backup_eligible
  }
  SESSION {
    string session_id PK
    string account_id FK
    string acr
    datetime auth_time
    datetime idle_expires_at
    datetime absolute_expires_at
  }
  REFRESH_FAMILY {
    string family_id PK
    string session_id FK
    string client_id FK
    string current_token_hash
    datetime expires_at
    string state
  }
  CLIENT {
    string client_id PK
    string tenant_id FK
    string type
    string redirect_uris
    string auth_method
  }
  CONSENT {
    string account_id PK
    string client_id PK
    string scopes
  }
  SIGNING_KEY {
    string kid PK
    string tenant_id FK
    string state
    datetime publish_at
    datetime retire_at
  }
```

### 5.2 Storage choices

| Data | Store | Why |
|---|---|---|
| Accounts, credentials, clients, consents | *Example:* Relational database, primary per region with replicas | Strong consistency, audited changes |
| Sessions, authorization codes | *Example:* In-memory replicated store with TTL | Short-lived, high read rate |
| Refresh families | *Example:* Key-value store with conditional writes and TTL | 4 000+ rotations/s |
| Signing keys | KMS-wrapped key material plus metadata table | Custody and rotation schedule |
| Audit events | Append-only log to the security data platform | Investigation; never on the request path |

### 5.3 Access patterns

| ID | Query | Key or index | Partition key rationale |
|---|---|---|---|
| AP-01 *(Example)* | Find account by login identifier | Unique index (tenant_id, normalized email) | Tenant-scoped uniqueness |
| AP-02 *(Example)* | Load passkey by credential ID during WebAuthn assertion | Primary key credential_id | Credential ID arrives from the authenticator |
| AP-03 *(Example)* | Redeem authorization code once | Key = hash(code), delete on read | Single use enforced atomically |
| AP-04 *(Example)* | Rotate refresh token | Key = hash(token) to family; conditional update on family_id | Family is the unit of revocation |
| AP-05 *(Example)* | Revoke all sessions and families of an account | Index account_id on SESSION and REFRESH_FAMILY | Needed on password reset and compromise |
| AP-06 *(Example)* | Validate client and redirect URI | Primary key client_id, cached | Every authorize call |
| AP-07 *(Example)* | Publish keys | SIGNING_KEY by tenant and state | Rarely changes; served from cache |

## 6. High-level design

<!-- Guidance: One diagram with the browser path, the back-channel path and where secrets live. -->

```mermaid
flowchart LR
  browser([Browser or app]) --> edge[Edge, bot and rate limits]
  edge --> login[Login and consent UI]
  edge --> as[Authorization and token service]
  login --> authn[Authenticator service: passkey, password, TOTP]
  authn --> hash[Isolated hashing pool]
  authn --> db[(Accounts and credentials DB)]
  login --> sess[(Session store)]
  as --> sess
  as --> rt[(Refresh family store)]
  as --> keys[Key service]
  keys --> kms[(KMS / HSM)]
  as --> jwks[JWKS and discovery, CDN cached]
  as --> fed[Federation broker to enterprise IdPs]
  as --> audit[[Audit events]]
  rs[Resource servers] --> jwks
  as -->|back-channel logout| clients[Relying parties]
```

| Component | Responsibility | Scales by |
|---|---|---|
| Edge | TLS, bot scoring, per-IP and per-account login limits | Edge provider |
| Login and consent UI | Render sign-in, step-up, consent and recovery pages | Stateless replicas |
| Authenticator service | Verify passkeys, passwords, TOTP; apply lockout rules | Stateless; hashing pool separate |
| Authorization and token service | Validate clients, issue codes and tokens, rotate refresh tokens | Stateless replicas |
| Key service | Generate, schedule and retire signing keys | Single leader per region |
| Federation broker | Act as relying party to enterprise IdPs and map claims | Per tenant configuration |

## 7. Detailed design

<!-- Guidance: Two to four deep dives on the hard problems, each with options, choice and reason. Pitfall: spending the review on the login page layout. -->

### 7.0 Authorization code flow with PKCE

Every client, public or confidential, uses this flow for user sign-in. The
IdP stores the code challenge with the code and accepts the code only once,
only from the same client, and only with the matching verifier.

```mermaid
sequenceDiagram
  participant U as Browser
  participant A as App (client)
  participant I as Identity provider
  participant R as Resource server
  A->>A: create code_verifier, code_challenge = S256(verifier)
  A->>U: redirect to /authorize (challenge, state, nonce)
  U->>I: GET /authorize
  I->>U: login page (passkey or password + second factor)
  U->>I: credentials
  I->>I: create session, store code with challenge (60 s, single use)
  I->>U: redirect to app with code and state
  U->>A: callback (code, state)
  A->>I: POST /token (code, verifier)
  I->>I: check S256(verifier) = challenge, redirect_uri matches
  I-->>A: ID token, access token, refresh token
  A->>R: API call with access token
  R->>R: verify signature with cached JWKS, check iss, aud, exp
```

### 7.1 Deep dive: token lifecycle and refresh rotation

| Token | Format | Lifetime (*Example*) | Bound to | Revocation |
|---|---|---|---|---|
| Authorization code | Opaque, stored as hash | 60 s, single use | Client, redirect URI, PKCE challenge | Consumed on first use |
| ID token | Signed JWT | 5 min | Client (`aud`), `nonce` | Not revoked; short-lived |
| Access token | JWT per RFC 9068, or opaque | 10 min | Audience (resource server), optionally DPoP key | Expiry; introspection for opaque |
| Refresh token | Opaque, stored as hash | 30 days absolute, 7 days idle | Client, session, family | Family revocation |

| Option for public-client refresh tokens | Verdict |
|---|---|
| A. Long-lived refresh token, no rotation | Rejected: RFC 9700 §2.2.2 requires sender-constraining or rotation for public clients |
| B. Rotation with reuse detection | **Chosen as baseline** |
| C. Sender-constrained with DPoP (RFC 9449) | **Chosen in addition** where the client library supports it |

**Choice and reason.** Every refresh returns a new refresh token and marks
the old one used, in one conditional write on the family. If a used token
is presented again, either an attacker or the legitimate client holds a
stolen copy; the IdP cannot tell which, so it revokes the whole family and
the session's other families for that client, forcing a new login. Allow a
grace window of a few seconds in which the same old token returns the same
new token, to tolerate a retry after a network failure without triggering
revocation.

```mermaid
stateDiagram-v2
  [*] --> Active: issued at login
  Active --> Used: rotated (new token issued)
  Used --> FamilyRevoked: presented again after grace window
  Active --> FamilyRevoked: logout, password reset, admin action
  Active --> Expired: idle or absolute lifetime reached
  FamilyRevoked --> [*]
  Expired --> [*]
```

### 7.2 Deep dive: signing key rotation and JWKS

| Option | How | Verdict |
|---|---|---|
| A. One key, rotate on incident only | Simple; a compromise forces emergency rotation that breaks clients | Rejected |
| B. Scheduled rotation with pre-publication | New key published in JWKS before use; old key kept for verification after retirement | **Chosen** |
| C. Per-token remote signing in KMS | Strong custody; latency and rate limits at 4 750 signatures/s | Rejected for tokens; used for key wrapping |

**Choice and reason.** Keys move through states: *pending* (published in
JWKS, not used), *active* (signs new tokens), *retired* (still published,
verifies tokens until they expire), *removed*. Publish a pending key at
least one JWKS cache lifetime plus a margin before activation, so every
resource server has fetched it. Keep the retired key published for at
least the longest token lifetime it signed. Resource servers select the key
by `kid` and refetch JWKS once when they see an unknown `kid`, with a rate
limit on refetches. For a compromised key, skip the pending period, remove
the key, and accept that tokens it signed are rejected.

| Step (*Example* schedule, 90-day rotation) | Day |
|---|---|
| Generate key N+1, publish as pending | 0 |
| Activate N+1; key N becomes retired | 2 |
| Remove key N from JWKS | 3 (after the longest lifetime it signed, plus margin) |

### 7.3 Deep dive: sessions, MFA, passkeys and step-up

- **Authenticators.** Offer passkeys (WebAuthn) as the primary factor.
  NIST SP 800-63B-4 requires verifiers at AAL2 to offer at least one
  phishing-resistant option, allows syncable authenticators (synced
  passkeys) up to AAL2, and requires non-exportable keys at AAL3. Use
  device-bound keys (security keys or platform keys flagged as not backup
  eligible) for administrators who need AAL3.
- **Passwords as fallback.** Follow SP 800-63B-4 §3.1.1: minimum 15
  characters when the password is the only factor, 8 when it is one factor
  of MFA, a blocklist of common and breached values, no composition rules,
  and no forced periodic changes. Hash with a memory-hard function; tune
  parameters to the CPU budget in 3.3.
- **Throttling.** Limit consecutive failures per account; SP 800-63B-4
  §3.2.2 caps them at 100. Combine with per-IP and per-device limits at the
  edge so throttling does not become a lockout weapon.
- **Sessions.** The browser session cookie is `Secure`, `HttpOnly`,
  `SameSite=Lax`, host-only. Apply an idle timeout and an absolute timeout;
  for AAL2 SP 800-63B-4 recommends reauthentication at least every 24 hours
  and after at most 1 hour of inactivity. A 30-day refresh token outlives
  that window, so for clients that claim AAL2 for their own sessions, cap
  the refresh family at the same absolute limit or require reauthentication
  with `max_age`.
- **Step-up.** A client asks for a higher level with `acr_values` or
  `max_age`. The IdP compares the session's `acr` and `auth_time` with the
  request and reauthenticates if needed, then reports `acr`, `amr` and
  `auth_time` in the ID token so the client can verify.

### 7.4 Deep dive: account recovery

| Option | Assurance | Verdict |
|---|---|---|
| A. Email link only | Equals the security of the mailbox | Allowed only for accounts without MFA |
| B. SMS code | Vulnerable to SIM swap | Rejected for recovery |
| C. Recovery codes issued at enrolment, or a second registered passkey | Equals the original factors | **Chosen** |
| D. Support-assisted recovery with identity verification and a waiting period | Slower; resists social engineering | **Chosen as last resort** |

**Choice and reason.** Recovery must not be easier than login. On every
recovery: notify all verified channels, revoke existing sessions and
refresh families (AP-05), apply a cooling-off delay before sensitive
changes such as adding a new passkey, and log the event for the security
team.

### 7.5 Multi-tenancy and federation

Consumers share one issuer. Each enterprise tenant gets its own issuer URL
(`https://id.example.com/t/{tenant}`), its own signing keys and its own
client registrations, so a token from one tenant can never validate for
another and a tenant's key can be rotated alone. For tenants that federate,
the broker acts as a relying party to their IdP, maps claims to local
accounts by a stable subject identifier (never by email alone), and applies
the tenant's MFA policy. See Multi-Tenancy Design Template
(`architecture/multi-tenancy-design`).

> **How real systems do it: Keycloak**
>
> The open-source Keycloak server organises identity into realms: each
> realm is an isolated space with its own users, credentials, roles and
> client applications, which is the same per-tenant isolation as 7.5. Its
> Server Administration Guide (version 26.8.1) explains that each realm
> signs with one active key pair while older passive keys stay available to
> verify earlier signatures, which lets administrators rotate without
> downtime. The guide's suggested procedure is to add a new key with a
> higher priority so it becomes active, keep the previous key passive for a
> transition period, and remove it later; it suggests rotation every few
> months and warns that sessions signed with a removed key need
> reauthentication. This box paraphrases the public documentation; read it
> for the exact settings.

## 8. Scalability and performance

<!-- Guidance: Show how the design meets section 3 and where it breaks first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Login bursts | Stateless UI, edge rate limits | Hashing pool size |
| Credential stuffing | Bot scoring and limits before hashing | Edge capacity |
| Refresh writes | Dedicated store, conditional writes | *Example:* Store write throughput per region |
| Token validation | Local JWT verification at resource servers | None at the IdP |
| Introspection | Only for opaque third-party tokens; cached for seconds | Introspection rate |
| JWKS | CDN with `Cache-Control: max-age` of minutes | None |

Latency budget for `POST /token` with refresh grant (target p99 150 ms):

| Step | Budget |
|---|---|
| TLS and client authentication | 20 ms |
| Load family and conditional rotate | 40 ms |
| Session check (still valid, not revoked) | 15 ms |
| Sign tokens | 5 ms |
| Audit event enqueue | 5 ms |
| Headroom | 65 ms |

## 9. Reliability, security and operations

<!-- Guidance: An IdP outage stops every login in the company. Name the failure, its blast radius and how fast you recover. -->

### 9.1 Failure modes

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Token service | *Example:* Region down | Synthetic login probes | New logins and refreshes in region | Active-active regions; clients keep valid access tokens until expiry | 5 min |
| Session store | *Example:* Data loss | Error rate, session count drop | Users must log in again | Replicas; treat loss as forced logout, never as anonymous access | 15 min |
| Refresh store | *Example:* Unavailable | Write errors | Refresh fails, users re-authenticate | Multi-AZ; fail closed (no refresh without rotation record) | 15 min |
| Signing key | *Example:* Suspected compromise | Security alert | All tokens signed with the key | Emergency rotation runbook; remove key from JWKS | 1 h |
| JWKS / CDN | *Example:* Stale cache | Unknown-`kid` errors at resource servers | Tokens from new key rejected | Pre-publication window; refetch on unknown `kid` | 15 min |
| Federation broker | *Example:* Tenant IdP down | Federation error rate per tenant | That tenant's users | Show status page; no fallback to weaker login | Depends on tenant |
| Hashing pool | *Example:* Saturated by attack | Queue depth | Password logins slow | Edge limits; passkey logins unaffected | 10 min |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [RTO] |

### 9.2 Security

| Threat | Control |
|---|---|
| Authorization code interception | PKCE S256 required; exact redirect URI match; codes single use |
| Mix-up and CSRF on callback | `state` checked; `iss` returned in the authorization response (RFC 9207) |
| Stolen refresh token | Rotation with reuse detection; DPoP binding where supported |
| Phishing of credentials | Passkeys are origin-bound; phishing-resistant option offered at AAL2 |
| Credential stuffing | Breached-password blocklist, edge limits, per-account throttling |
| Token replay at the wrong API | `aud` restricted per resource server (RFC 9700 §2.3) |
| Insider access to keys | Keys wrapped by KMS, access audited, two-person rule for key export |
| Account takeover via recovery | 7.4 controls; notification on every recovery |
| [Threat] | [Control] |

Run the full analysis in Threat Model Template (`security/threat-model`).

### 9.3 Observability

| Signal | Metric | Alert threshold |
|---|---|---|
| Login success rate | Per method and tenant | *Example:* Drop of 10 points vs. 7-day baseline |
| Failed logins | Per IP range and per account | Spike above baseline x 5 |
| Refresh reuse detections | Count per client | Any sustained increase |
| Unknown `kid` reports | From resource server libraries | Any after rotation |
| Token endpoint latency | p99 | Above 150 ms for 10 min |
| Recovery events | Per day | Above baseline x 3 |

### 9.4 Cost estimate

| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Token and UI services | [USD per instance-month] | [Count] | [USD] |
| Hashing pool | [USD per core-month] | *Example:* 24 cores peak, autoscaled | [USD] |
| Session and refresh stores | [USD per node-month] | [Count] | [USD] |
| KMS / HSM | [USD per key and per operation] | [Keys, operations] | [USD] |
| SMS and email for notifications | [USD per message] | [Volume] | [USD] |
| **Total** | | | **[USD]** |

## 10. Trade-offs and alternatives

<!-- Guidance: For each major decision list the options and the cost of the choice. Include what changes at 10x and 0.1x scale. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Build or adopt | Build; open-source server; managed IdP | [Decision] | [Reason] | [Cost] |
| Access token format | JWT; opaque with introspection | JWT for first-party, opaque for third-party | Local validation at scale; revocable where it matters | JWT cannot be revoked before expiry |
| Refresh protection | Rotation; DPoP; mTLS (RFC 8705) | Rotation plus DPoP where possible | Works for browsers and mobile | Retry handling complexity |
| Primary factor | Password + OTP; passkeys | Passkeys first | Phishing resistance, fewer resets | Device and recovery edge cases |
| Tenant model | Shared issuer with tenant claim; issuer per tenant | Issuer per enterprise tenant | Strong isolation, per-tenant keys | More metadata and keys to manage |
| **At 10x** (50 million DAU) | | | Refresh store becomes the hot spot: partition by family, consider longer access tokens with DPoP; dedicated regions for large tenants | |
| **At 0.1x** (500 000 DAU) | | | A managed IdP or a single open-source cluster is enough; skip custom key service and use the product's rotation | |

## 11. Delivery plan and risks

<!-- Guidance: Migration of existing users and password hashes is the longest task; plan for users who never come back to convert. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Issuer, code flow with PKCE, passwords, TOTP | Conformance test suite for OpenID Connect passes | YYYY-MM-DD |
| M2 | First product migrated; refresh rotation | Reuse detection drill passes | YYYY-MM-DD |
| M3 | Passkeys and recovery | Recovery abuse test passes | YYYY-MM-DD |
| M4 | All products; password grant removed | No product uses legacy login | YYYY-MM-DD |
| M5 | Enterprise tenants and federation | First tenant live | YYYY-MM-DD |

### 11.2 Migration and rollback

[*Example:* Existing password hashes are imported with their algorithm tag
and re-hashed with the new function on next successful login. Each product
switches login with a feature flag; rollback returns to its legacy login
while keeping IdP accounts in sync for 30 days.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* IdP outage blocks all products | Medium | High | Active-active regions; game days | [@name] |
| R-02 | *Example:* Mobile apps cannot drop the password grant on time | Medium | Medium | Native SDK with system browser and PKCE | [@name] |
| R-03 | *Example:* Passkey recovery generates support load | High | Medium | Encourage two passkeys; recovery codes | [@name] |
| R-04 | *Example:* Key rotation breaks third-party clients that pin keys | Medium | High | Document JWKS usage; pre-publication window | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Traceability, open questions, glossary and references. -->

### 12.1 Traceability

| Requirement | Design element | Verification |
|---|---|---|
| FR-01 | 4.1, 7.0 | OpenID Connect conformance tests |
| FR-04, NFR-05 | 7.1 | Reuse detection test |
| FR-05, NFR-04 | 7.3 | Authenticator inventory review |
| FR-08 | 7.4 | Recovery abuse test |
| NFR-06 | 7.2 | Rotation drill |

### 12.2 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Do we accept synced passkeys for administrators? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* When do we adopt OAuth 2.1 once it is published as an RFC? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.3 Glossary

| Term | Meaning |
|---|---|
| IdP | Identity provider: authenticates users and issues identity assertions |
| Relying party (RP) | Application that trusts the IdP's tokens |
| PKCE | Proof Key for Code Exchange: binds the code to the client instance |
| JWKS | JSON Web Key Set: the published public keys |
| AAL | Authenticator assurance level in NIST SP 800-63B-4 |
| `acr` / `amr` | Authentication context class and methods reported in the ID token |
| Refresh family | Chain of refresh tokens descending from one login |
| DPoP | Demonstrating Proof of Possession: binds tokens to a client key |

### 12.4 References

| Reference | Use |
|---|---|
| OpenID Connect Core 1.0 incorporating errata set 2 (15 December 2023) | ID token, claims, flows |
| OpenID Connect Discovery 1.0; RP-Initiated Logout 1.0; Back-Channel Logout 1.0 | Metadata and logout |
| RFC 6749 OAuth 2.0; RFC 7636 PKCE; RFC 7009 Revocation; RFC 7662 Introspection | Core protocol |
| RFC 9700 Best Current Practice for OAuth 2.0 Security (BCP 240, January 2025) | Grant restrictions, redirect matching, refresh protection |
| RFC 9068 JWT access tokens; RFC 9449 DPoP; RFC 9126 PAR; RFC 9207 issuer identifier; RFC 7517 JWK; RFC 8414 metadata; RFC 8176 `amr` values | Token and transport details |
| NIST SP 800-63-4 Digital Identity Guidelines (final, July 2025), part SP 800-63B-4 | Assurance levels, passwords, throttling, reauthentication |
| W3C Web Authentication Level 3 (W3C Recommendation, 25 August 2026) | Passkeys |
| Keycloak Server Administration Guide 26.8.1 | 7.5 box |
| OAuth 2.1 Internet-Draft (draft-ietf-oauth-v2-1) | Direction of travel; still a draft |

## Standard and licence note

Based on OpenID Connect Core 1.0 incorporating errata set 2, the IETF OAuth
RFCs listed in 12.4 (including RFC 9700, January 2025), NIST SP 800-63-4
(July 2025) and W3C Web Authentication Level 3. This is a reference design
with illustrative numbers; it is not a conformance claim. Specifications and
NIST guidance are revised; check the current official versions, including
the status of the OAuth 2.1 draft, before implementing. Original text
written for this collection. Template licence: MIT.
