OAuth2 / OIDC Authorization Server
OAuth2 / OIDC Authorization Server #
This note models an OAuth2/OIDC authorization server where clients initiate authorization flows, users authenticate and grant consent, the server issues authorization codes and tokens, and refresh, revocation, introspection, and key rotation are handled safely at scale.
Step 1 - Normalize #
Assume the baseline prompt is:
- design an OAuth2 / OIDC authorization server
- users authenticate and authorize client apps
- server issues authorization codes, access tokens, ID tokens, and refresh tokens
- clients redeem, refresh, revoke, and introspect tokens
- consent, scopes, client trust, and signing keys matter
- system scales across many tenants and apps
Normalize into state-affecting paths.
| Requirement | Actor | Operation | State touched | Priority |
|---|---|---|---|---|
| Client starts authorization flow | Client | append event | S1create targetAuthorizationRequest | C1 |
| Service authenticates user and verifies policy | System | state transition | S1update targetAuthorizationFlowState | C1 |
| User grants or denies consent | Client | state transition | S1update targetConsentGrantState | C1 |
| Service issues authorization code | System | append event | S1create targetAuthorizationCode | C1 |
| Client exchanges code for tokens | Client | state transition | S1update targetTokenGrantState | C1 |
| Service issues access / ID / refresh tokens | System | append event | S1create targetIssuedToken | C1 |
| Client refreshes token | Client | state transition | S1update targetTokenGrantState | C1 |
| Client or admin revokes token / grant | Client | state transition | S1update targetTokenGrantState | C1 |
| Client introspects token / userinfo / JWKS | Client | read source | S1read source targetTokenGrantState | R1 |
| Admin updates client config / redirect URIs / scopes | Admin | overwrite state | S1update targetClientConfig | C1 |
| Admin updates auth policy | Admin | overwrite state | S1update targetAuthorizationPolicy | C1 |
| System rotates signing keys | System | state transition | S1update targetSigningKeyState | C1 |
| User reads consent/session activity | Client | read projection | S1read projection targetAuthorizationActivityView | R2 |
| System routes tenant/shard to current owner | System | read source | S1read source targetPartitionMap | C1 |
| System reassigns shard ownership after node failure | System | state transition | S1update targetPartitionOwnership | C1 |
Notes on normalization #
Important choices:
- auth flow start is
append event- authorization intent is an immutable interaction fact
- auth/consent progression is
state transition- current flow and grant lifecycle evolve
- authorization code and token issuance are
append event- each credential issuance is a fact
- code exchange, refresh, and revocation change current grant state
- client config and policy are current-value control state
- key rotation is explicit because signed token validity depends on it
This system is a hybrid of:
delegated authorization flowgrant/session lifecycle statesigned token issuance
Step 2 - Critical Path Selection #
| Requirement | Priority class | Why |
|---|---|---|
| Start authorization flow | C1 | flow intent and redirect correlation must be preserved |
| Authenticate user / verify policy | C1 | wrong authorization decision is a security failure |
| Grant or deny consent | C1 | consent correctness affects delegated access |
| Issue authorization code | C1 | code issuance is core protocol output |
| Exchange code for tokens | C1 | token issuance must be bound to current valid code/grant |
| Refresh token | C1 | refresh semantics determine long-lived access correctness |
| Revoke token / grant | C1 | revocation correctness affects security |
| Introspect token / userinfo / JWKS | R1 | core serving path |
| Update client config / policy | C1 | future auth decisions and redirect validation depend on current config |
| Rotate signing keys | C1 | key lifecycle controls token verification trust |
| Route to shard owner | C1 | wrong routing can split grant/session truth |
| Reassign shard ownership | C1 | failover must preserve authorization correctness |
Baseline critical paths #
Main C1 paths:
P1start authorization flowP2authenticate and verify policyP3grant or deny consentP4issue authorization codeP5exchange code for tokensP6refresh tokenP7revoke token or grantP8update client config / policyP9rotate signing keysP10route to shard ownerP11reassign shard ownership
Main R1 path:
P12introspect / validate / userinfo / JWKS reads
This design is driven by:
- authoritative current grant state
- safe issuance of one-time codes and signed tokens
- current client trust and consent state
- revocation and key rotation correctness
Step 3 - Primary State Extraction #
For an OAuth2/OIDC authorization server, the minimal primary state is the authorization request, flow lifecycle, consent grant state, token grant state, issued credentials, client config, policy, signing key lifecycle, and routing/ownership state.
| Candidate object label | Candidate source | Candidate needed for C1/R1? | Candidate decomposition action | Class | Primary? | Owner | Evolution | Scope kind | Scope value |
|---|---|---|---|---|---|---|---|---|---|
| AuthorizationRequest | direct noun | Yes | keep as candidate | event | Yes | service | append-only | instance | auth_request_id |
| AuthorizationFlowState | lifecycle object | Yes | keep as candidate | process | Yes | service | state machine | instance | flow_id |
| ConsentGrantState | lifecycle object | Yes | keep as candidate | process | Yes | service | state machine | instance | user_id + client_id + scope_set |
| AuthorizationCode | direct noun | Yes | keep as candidate | event | Yes | service | append-only | instance | code_id |
| TokenGrantState | lifecycle object | Yes | keep as candidate | process | Yes | service | state machine | instance | grant_id |
| IssuedToken | direct noun | Yes | keep as candidate | event | Yes | service | append-only | instance | token_id or jti |
| ClientConfig | direct noun | Yes | keep as candidate | entity | Yes | service | overwrite | instance | client_id |
| AuthorizationPolicy | direct noun | Yes | keep as candidate | entity | Yes | service | overwrite | instance | tenant_id or policy_scope |
| SigningKeyState | lifecycle object | Yes | keep as candidate | process | Yes | service | state machine | instance | key_id |
| PartitionOwnership | hidden write target | Yes | keep as candidate | process | Yes | service | state machine | instance | shard_id |
| PartitionMap | hidden write target | Yes | keep as candidate | entity | Yes | service | overwrite | collection | tenant/shard map |
| AuthorizationActivityView | derived read model | No | reject as UI artifact | projection | No | derived | overwrite | collection | user_id or tenant |
Important modeling choices #
AuthorizationFlowState #
Primary because:
- flows often span multiple steps:
- request received
- user authenticated
- consent pending
- consent granted/denied
- code issued
ConsentGrantState #
Primary because:
- consent may be reusable and revocable
- current user/client/scope authorization matters for future flows
TokenGrantState #
Primary because:
- current grant lifecycle governs:
- code redeemed or not
- refresh-token active or rotated
- revoked or expired
IssuedToken #
Primary because:
- each issued token/ID token is an immutable fact
- useful for audit, jti tracking, replay protection, and revocation metadata
SigningKeyState #
Primary because:
- key lifecycle matters:
- active
- next
- retired
- revoked
Minimal strict primary set #
The strongest minimal set is:
AuthorizationRequestAuthorizationFlowStateConsentGrantStateAuthorizationCodeTokenGrantStateIssuedTokenClientConfigAuthorizationPolicySigningKeyStatePartitionOwnershipPartitionMap
Step 4 - Hard Invariants #
For an OAuth2/OIDC authorization server, the hard invariants are about valid client trust/config, guarded flow and grant transitions, one-time code redemption, correct token issuance, and safe revocation/key rotation.
| Path | Tier | Type | Invariant statement |
|---|---|---|---|
P1 start authorization flow | HARD | uniqueness | Key auth_request_id maps to at most one logical outcome recorded authorization request within flow scope. |
P2 authenticate and verify policy | HARD | eligibility | Action advance_authorization_flow is valid only if current AuthorizationFlowState, ClientConfig, and AuthorizationPolicy allow the transition at decision time. |
P3 grant or deny consent | HARD | eligibility | Action advance_consent_grant is valid only if current flow state, user identity, and requested scopes allow consent transition at decision time. |
P4 issue authorization code | HARD | eligibility | Action issue_authorization_code is valid only if current flow state is issuable, redirect/client binding is valid, and current policy allows issuance at decision time. |
P5 exchange code for tokens | HARD | eligibility | Action redeem_code_for_tokens is valid only if the code exists, is unredeemed, unexpired, correctly bound to client/redirect/PKCE context, and current SigningKeyState is active at decision time. |
P5 exchange code for tokens | HARD | uniqueness | Key authorization_code_id maps to at most one logical outcome successful redemption within code scope. |
P6 refresh token | HARD | eligibility | Action refresh_grant is valid only if current TokenGrantState is active, refresh token is valid/unrevoked, and rotation policy allows the transition at decision time. |
P7 revoke token or grant | HARD | eligibility | Action revoke_grant is valid only if current TokenGrantState allows revocation at decision time. |
P8 update client config / policy | HARD | ordering | Client-config and policy revisions are ordered by monotonic version within their scopes. |
P9 rotate signing keys | HARD | eligibility | Action advance_signing_key_state is valid only if current SigningKeyState lifecycle and trust-publication rules allow the transition at decision time. |
P10 route to shard owner | HARD | uniqueness | Key shard_id maps to at most one logical outcome current authoritative owner within shard_id. |
P11 reassign shard ownership | HARD | eligibility | Action reassign_shard is valid only if current owner is failed or relinquished and candidate owner is eligible and sufficiently current on shard_id at decision time. |
P12 introspect / validate | HARD | freshness | Validation, introspection, and userinfo reflect authoritative grant/session/key state within configured consistency bound. |
What matters most #
1. Authorization code redemption is one-time #
This is one of the central protocol invariants.
2. Token issuance is guarded by current flow, client, and key state #
Wrong redirect/client binding or wrong key state breaks security.
3. Refresh-token lifecycle is current state #
Rotation, revocation, reuse detection, and expiry all depend on the authoritative current grant state.
4. Revocation must affect future introspection #
If a grant is revoked, future refresh and introspection must reflect it.
Step 5 - Execution Context #
For the baseline OAuth2/OIDC authorization server:
| Field | Value | Why |
|---|---|---|
| Topology | single service distributed | one logical authorization server spread across auth, grant, and config nodes |
| Write coordination scope | per object scope | correctness is per auth flow, consent grant, token grant, client config, key lifecycle, and shard ownership scope |
| Read consistency target | strong only | authorization, token issuance, revocation, and introspection are security-critical |
| Holder model | client | user/browser and client app drive grant/session lifecycle through current server-side state |
| Compensation acceptable? | No | wrong token issuance or stale revocation cannot be safely repaired afterward |
Derived implications #
holder_may_crash = true- browser/client flows can disappear mid-flow, and nodes can fail mid-grant transition
cross_service_write = false- baseline keeps auth, consent, grant, trust config, and key state in one logical service
bounded_staleness_allowed = false- security-critical reads should use authoritative state
cross_service_atomicity_required = false- no multi-service transaction across unrelated services in baseline
exclusive_claim_required = true- shard ownership must be exclusive
guarded_by_current_state = true- auth, consent, code redemption, refresh, revocation, and key rotation all depend on current state
What this implies #
This pushes us toward:
- one authoritative owner per tenant/grant shard
- append-oriented auth-request, code, and token issuance records
- current-value client config and policy
- guarded flow/grant/key lifecycle transitions
Step 6 - Deterministic Mechanism Selection #
| Path | Write shape | Base mechanism | Required companions |
|---|---|---|---|
P1 start authorization flow | append-only event | append log | correlation id, state token |
P2 authenticate / verify policy | guarded state transition | CAS on (state, version) or single writer per shard | flow version, PKCE/state binding |
P3 grant or deny consent | guarded state transition | CAS on (state, version) | consent version |
P4 issue authorization code | append-only event guarded by current state | one-time code issuance | redirect binding, PKCE metadata |
P5 exchange code for tokens | guarded state transition plus append issuance | CAS on grant/code state, then signed token issuance | one-time redemption marker, active key |
P6 refresh token | guarded state transition plus append issuance | CAS on grant state | rotation/reuse detection, active key |
P7 revoke token / grant | guarded state transition | CAS on (state, version) | grant version |
P8 update client config / policy | overwrite current value | CAS on version | config/policy version |
P9 rotate signing keys | guarded state transition | lifecycle state transition | key version, JWKS publication epoch |
P10 route to shard owner | exclusive claim | lease | fencing token, heartbeat |
P11 reassign shard ownership | guarded state transition | CAS on (state, version) | fencing token, shard catch-up check |
Why these fit #
Flow, consent, and grant lifecycle #
These depend on current state, factors, and policy, so guarded transitions fit.
Code and token issuance #
Issuance creates immutable credentials, but only under current valid flow/grant/client/key state.
Client config and policy #
These are current-value control state, so overwrite fits.
Key rotation #
This is lifecycle-managed current state, so guarded transition fits.
Canonical substrate implied #
The baseline now points to:
- sharded authorization-server service
- one owner per tenant/grant shard
- current flow, consent, and grant lifecycle state
- append-only code/token issuance records
- current client config, policy, and signing-key lifecycle
Step 7 - Read Model / Source of Truth #
For an OAuth2/OIDC authorization server, truth is mostly direct source state. Activity and audit views are derived.
| Concept | Truth | Read path | Rebuild path |
|---|---|---|---|
C1 authorization flow initiation | AuthorizationRequest | read source directly | authoritative auth-request store |
C2 current authorization flow lifecycle | AuthorizationFlowState | read source directly | authoritative flow-state store |
C3 consent grant lifecycle | ConsentGrantState | read source directly | authoritative consent store |
C4 authorization code issuance | AuthorizationCode | read source directly | authoritative code store |
C5 current token grant lifecycle | TokenGrantState | read source directly | authoritative grant store |
C6 issued token history | IssuedToken | read source directly | authoritative issuance/audit store |
C7 current client trust config | ClientConfig | read source directly | authoritative client-config store |
C8 current auth policy | AuthorizationPolicy | read source directly | authoritative policy store |
C9 signing key lifecycle | SigningKeyState | read source directly | authoritative key store |
C10 shard ownership | PartitionOwnership | read source directly | authoritative ownership store |
C11 shard routing map | PartitionMap | read source directly | authoritative routing metadata |
C12 activity / audit views | derived from requests, grants, and issuance | materialized view | recompute from authoritative state |
Important point #
For the core semantics:
- authorization and issuance read authoritative client config, policy, and key state
- introspection reads authoritative grant/session state
- audit/activity views are projections
Step 8 - Failure Handling #
| Path | Retry | Competing writers | Crash after commit | Publish failure | Stale holder |
|---|---|---|---|---|---|
P1 start authorization flow | retry safe with state/correlation token | parallel flows coexist per user/client | committed request survives crash if persisted | browser redirect may retry | stale shard owner blocked by fencing token |
P2 authenticate / verify policy | retry safe with flow version/challenge token | stale or duplicate response loses guarded transition | committed flow transition survives crash if persisted | external MFA/send step may retry | stale shard owner blocked by fencing token |
P3 grant or deny consent | retry with consent/flow version | stale consent write loses guarded transition | committed consent survives crash if persisted | browser post may retry | stale shard owner blocked by fencing token |
P4 issue authorization code | retry may create multiple codes unless flow is fenced and one-time issuance is enforced | issuance must be fenced by current flow state | committed code survives crash if persisted | browser redirect may retry | stale issuer blocked by ownership/version discipline |
P5 exchange code for tokens | retry with same code may race; only first successful redemption should win | stale or duplicate redemption loses one-time code transition | committed token issuance survives crash if audit persisted | client may not receive response even after issuance | stale issuer blocked by ownership/version discipline |
P6 refresh token | retry can create duplicate refresh outcomes unless rotation policy and grant version are fenced | stale refresh loses guarded transition | committed refresh survives crash if persisted | client may retry after lost response | stale issuer blocked by ownership/version discipline |
P7 revoke token / grant | retry with grant version | stale revoke loses guarded transition | committed revocation survives crash if persisted | introspection propagation must reflect authoritative state | n/a |
P8 update client config / policy | retry with config version | stale update loses CAS | committed config survives crash if persisted | config propagation may lag | n/a |
P9 rotate signing keys | retry with key version/lifecycle epoch | stale rotation loses guarded transition | committed key transition survives crash if persisted | JWKS propagation may lag | n/a |
P10 route to shard owner | retry after refreshing shard map | only one valid owner should exist | if owner changed, refreshed map points to new owner | n/a | stale owner rejected by fencing token |
P11 reassign shard ownership | retry failover transition safely | only one reassignment wins current ownership state | promoted owner crash triggers later reassignment | n/a | old owner fenced and must not continue serving |
P12 introspect / validate / userinfo | read retry safe | many readers coexist | node crash drops request only | relying party may retry | stale validation forbidden beyond consistency bound |
What matters most #
1. Authorization code redemption must be one-time #
Otherwise replay breaks the protocol.
2. Refresh-token rotation must be fenced #
Lost responses and replayed refreshes are common failure cases.
3. Issuance and delivery are separate #
The server may issue tokens successfully even if the client never receives the response.
4. Key rotation needs overlap #
New JWKS material must be available before old keys retire.
Step 9 - Scale Adjustments #
| Hotspot | Type | First response |
|---|---|---|
| login / authorization spikes | write throughput hotspot | shard by tenant/user/grant scope and add more auth nodes |
| hot grant/session tenants | contention hotspot | isolate large tenants and partition grant state more finely |
| client-config and JWKS reads on hot path | read hotspot | cache config and key material with strict versioning and short refresh bounds |
| introspection volume | read hotspot | use fast grant/session shards or short-lived self-contained access tokens where appropriate |
| audit/activity queries | read hotspot | serve from projections, not hot auth path |
| refresh storms | contention hotspot | jitter refresh windows and enforce rotation/reuse detection efficiently |
What scales well #
This system scales by:
- sharding grant and tenant state
- separating hot authorization/token reads from audit projections
- caching client config and JWKS carefully under strict version control
- using short-lived access tokens to reduce introspection and revocation pressure
What fails first #
Usually:
- login or consent spikes
- very large tenants with hot grant stores
- mismanaged refresh-token rotation
- introspection load from many relying parties
Canonical design conclusion #
The mechanical outcome is:
- primary state:
AuthorizationRequestAuthorizationFlowStateConsentGrantStateAuthorizationCodeTokenGrantStateIssuedTokenClientConfigAuthorizationPolicySigningKeyStatePartitionOwnershipPartitionMap
- critical invariants:
- guarded authorization-flow, consent, and grant lifecycle
- one-time code redemption
- issuance valid only under current grant, client config, policy, and active key state
- refresh/revocation reflected in future introspection
- exclusive shard ownership for grant truth
- mechanisms:
append log- guarded flow/grant/key transitions
- current-value client config and policy
- signed credential issuance
- fenced shard ownership
- reads:
- direct authoritative reads for authorization, introspection, and trust decisions
- projections for activity and audit UX
Polished interview answer #
I’d build the OAuth2/OIDC authorization server as a sharded grant-management service with one authoritative owner per tenant or grant shard. Authorization requests are recorded as flow facts, authentication and consent advance guarded flow and consent state machines, and successful flows issue one-time authorization codes bound to client, redirect, and PKCE context. Code exchange and refresh operate on authoritative grant state, so redemption, rotation, reuse detection, and revocation are all guarded transitions. Access tokens, refresh tokens, and ID tokens are issued as signed immutable credentials, but only if current client config, policy, consent, grant, and signing-key state all allow issuance. The main scaling levers are more grant shards, careful caching of client config and JWKS, short-lived access tokens, and keeping audit/activity views off the hot token path.
Concrete Substrate #
I’ll choose a sharded authorization-server service with durable flow/grant state plus separate signing-key and client-config stores as the concrete baseline, because it matches the mechanics we derived:
- append-only auth requests, codes, and token issuance records
- guarded flow, consent, and grant lifecycle
- current-value client config and policy
- managed signing-key lifecycle
- one owner per shard
Concrete tech family:
- authorization service in
Go,Java, orRust - durable metadata/state storage:
- replicated DB or
RocksDB-backed service state
- replicated DB or
- shard replication:
Raftor leader-follower replication with commit index
- signing:
- HSM/KMS-backed keys or managed signing service
- metadata/control:
etcdor internal metadata quorum for shard ownership/routing
Each shard owner stores:
- authorization-request log for owned scope
- current flow state
- current consent state
- current grant state
- code and issuance history / audit references
- current client config and policy cache
- key-reference metadata for active signing material
Operation Layer #
1. Start authorization flow #
API
StartAuthorization(client_id, redirect_uri, scope, state, code_challenge, prompt, context)
Initiator
- client app / browser
Entry point
- authorization frontend
Authoritative decider
- shard owner for tenant/client/user context
Precondition
- client config exists and redirect URI is valid
Transition
- append
AuthorizationRequest - create
AuthorizationFlowState = STARTED
Response
- login/consent redirect or next auth step
2. Authenticate and capture consent #
API
AdvanceAuthorizationFlow(flow_id, factor_response, consent_response, expected_version?)
Initiator
- user/browser
Entry point
- authorization frontend
Authoritative decider
- shard owner for auth flow
Precondition
- current flow state accepts this transition
- client config and policy allow requested scopes
Transition
- guarded update of
AuthorizationFlowState - guarded update of
ConsentGrantStateif consent is granted or denied
3. Issue authorization code #
API
IssueAuthorizationCode(flow_id, expected_version?)
Initiator
- system after successful auth + consent
Entry point
- authorization frontend
Authoritative decider
- shard owner for flow/grant
Precondition
- current flow state is issuable
- redirect/client/PKCE context valid
Transition
- append
AuthorizationCode - advance flow state to issued
4. Redeem code for tokens #
API
RedeemAuthorizationCode(client_id, code, code_verifier, redirect_uri)
Initiator
- client app/backend
Entry point
- token endpoint
Authoritative decider
- shard owner for code/grant plus active key state
Precondition
- code exists, unredeemed, unexpired, and bound to this client/redirect/PKCE context
Transition
- guarded update of
TokenGrantState - append
IssuedTokenrecords - sign and return access token / refresh token / ID token as appropriate
5. Refresh token #
API
RefreshToken(client_id, refresh_token)
Initiator
- client app/backend
Entry point
- token endpoint
Authoritative decider
- shard owner for grant plus active key state
Precondition
- current grant state is refreshable
- refresh token valid and unrevoked
Transition
- guarded update of
TokenGrantState - append new
IssuedTokenrecords - optionally rotate refresh token and invalidate old one
6. Revoke token / grant #
API
RevokeGrant(token_or_grant_id, actor, expected_version?)
Initiator
- client, user, or admin
Entry point
- revocation endpoint / admin API
Authoritative decider
- shard owner for grant
Precondition
- current grant state is revocable
Transition
- guarded update
TokenGrantState -> REVOKED
7. Rotate signing key #
API
- internal admin/key-management flow
Initiator
- system/admin
Entry point
- key-management API
Authoritative decider
- key-state owner
Precondition
- new key material available
- JWKS/publication rules satisfied
Transition
SigningKeyState: NEXT -> ACTIVE- prior active key moves to retiring/retired later
Entry Point vs Decider vs Responder #
| Path | Entry point | Authoritative decider | Physical responder | Logical responder |
|---|---|---|---|---|
| start authorization | authorization frontend | flow/grant shard owner | frontend node | authorization server |
| advance auth/consent | authorization frontend | flow shard owner | frontend node | authorization server |
| redeem code / refresh | token endpoint | grant shard owner + active key state | token node | authorization server |
| revoke grant | revocation endpoint | grant shard owner | API node | authorization server |
| rotate key | key-management API | key-state owner | control-plane node | authorization server |
| shard failover | follower / coordination layer | shard quorum / lease store | new leader / control plane | authorization server |
Concrete HLD #
Main components:
- authorization frontend
- handles browser redirects, consent pages, and callbacks
- token endpoint / grant shard owners
- authoritative owners for code/grant lifecycle and token issuance
- client-config / policy service
- stores tenant policy and client metadata
- signing/key service
- manages active/next/retired signing material and JWKS
- metadata/control service
- tracks shard ownership and routing
- audit/activity pipeline
- serves activity views and compliance reporting
Short Interview Version #
I’d build the OAuth2/OIDC authorization server as a sharded grant-management service with one authoritative owner per tenant or grant shard. Authorization requests are recorded as flow facts, authentication and consent advance guarded flow and consent state machines, and successful flows issue one-time authorization codes bound to client, redirect, and PKCE context. Code exchange and refresh operate on authoritative grant state, so redemption, rotation, reuse detection, and revocation are all guarded transitions. Access tokens, refresh tokens, and ID tokens are issued as signed immutable credentials, but only if current client config, policy, consent, grant, and signing-key state all allow issuance. The main scaling levers are more grant shards, careful caching of client config and JWKS, short-lived access tokens, and keeping audit/activity views off the hot token path.