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.
Document control
| 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
| 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
1. Overview
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
| 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
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
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 | pl | Example: 5 | Morning peak |
| Active hours per active user per day | u | Example: 4 | Session logs |
| Access token lifetime | ta | Example: 10 min | 7.1 |
| Peak-to-average factor for refreshes | pr | 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
Lpeak=86400D×l×pl=864005×106×2×5≈116×5=580 logins/s Consequence: the login front end is small; capacity planning focuses on
the hashing pool below.
3.3 Password hashing CPU
H=Lpeak×q×c=580×0.6×0.05 s≈17.4 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
Rpeak=86400D×u×ta60×pr=864005×106×4×6×3≈1389×3≈4170 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+4170≈4750/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
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.
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
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):
{"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
5.1 Entities
Accounts own credentials and sessions; clients own grants; signing keys
are global per issuer.
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
| 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
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.
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.
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
| 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
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
| 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
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
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.