Skip to main content

OIDC and OAuth 2.0: An Architecture Deep-Dive

About the author: I'm Charles Sieg, a cloud architect and platform engineer who builds apps, services, and infrastructure for Fortune 1000 clients through Vantalect. If your organization is rethinking its software strategy in the age of AI-assisted engineering, let's talk.

I have built OAuth integrations across web browsers, Electron desktop apps, and native iOS applications. The same protocol, three completely different implementation patterns, three different ways to store tokens, three different ways things break in production. Most documentation treats OAuth as a single flow you bolt onto your app. That works until you ship a second platform and discover that browser redirects, custom protocol handlers, and ASWebAuthenticationSession have almost nothing in common except the access token at the end. This is the reference I wish I had before building multi-platform auth from scratch: how the protocol stack actually works, how tokens move through the system, and where each platform diverges in ways that matter.

A massive ornate vault door standing partially open, revealing a luminous network of interconnected golden keys and tokens floating in a dark chamber beyond, with beams of light streaming through the gap
A massive ornate vault door standing partially open, revealing a luminous network of interconnected golden keys and tokens floating in a dark chamber beyond, with beams of light streaming through the gap

The Protocol Stack: OAuth 2.0, OIDC, and OAuth 2.1

Three specifications, layered on top of each other. Every confusion about "OAuth vs. OIDC" stems from not understanding what each layer does.

LayerSpecificationPurposeKey Artifact
AuthorizationOAuth 2.0 (RFC 6749)Delegated access to resourcesAccess token
IdentityOpenID Connect Core 1.0Authentication and identity claimsID token (JWT)
ConsolidationOAuth 2.1 (draft-ietf-oauth-v2-1)Security hardening of OAuth 2.0Same tokens, stricter rules

OAuth 2.0 answers: "Can this application access this resource on behalf of this user?" It says nothing about who the user is. You get an access token. That token grants access to a protected resource. The protocol does not define the token format, does not require the client to understand the token, and does not provide user identity information.

OIDC adds an identity layer on top of OAuth 2.0. It answers: "Who is this user?" You get an ID token (always a JWT) containing claims about the user: subject identifier, email, name, authentication time. OIDC uses OAuth 2.0 as its authorization framework but adds the openid scope, the ID token, and the UserInfo endpoint.

OAuth 2.1 is not a new protocol. It consolidates a decade of security best practices into a single specification. The draft removes dangerous grant types, mandates PKCE, and tightens redirect URI validation.

What OAuth 2.1 Removes

Removed FeatureReasonReplacement
Implicit grant (response_type=token)Tokens exposed in URL fragments, browser history, server logsAuthorization Code + PKCE
Resource Owner Password Credentials (ROPC)Client handles raw credentials; no MFA supportAuthorization Code + PKCE
Bearer tokens in query stringsTokens logged in server access logs, proxy logs, referrer headersAuthorization header only
Loose redirect URI matchingEnables open redirect attacks, path confusion, parameter pollutionExact string matching required

If you are starting a new project today, build to OAuth 2.1 requirements. PKCE for every client. No implicit flow. No password grants. Exact redirect URI matching. The draft has been stable since late 2024 and every major identity provider already enforces these constraints.

Authorization Code Flow with PKCE

The Authorization Code flow with Proof Key for Code Exchange is the only grant type you should use for interactive user authentication. PKCE was originally designed for mobile apps (RFC 7636) but OAuth 2.1 mandates it for all clients, including confidential web servers, because it prevents authorization code injection attacks regardless of client type.

Generate code_verifier (random 43-128 chars)Compute code_challenge = SHA256(code_verifier)GET /authorize?response_type=code&client_id=xxx&redirect_uri=xxx&scope=openid profile email&state=random&code_challenge=xxx&code_challenge_method=S256Login prompt + consent screenAuthenticate + authorizeRedirect to redirect_uri with ?code=xxx&state=xxxVerify state matchesPOST /token with code + code_verifier + client_idVerify SHA256(code_verifier) == code_challengeReturn access_token + id_token + refresh_tokenAPI request with Authorization: Bearer access_tokenProtected resourceUserClient AppAuthorization ServerResource Server
Authorization Code flow with PKCE

Why PKCE Matters

Without PKCE, an attacker who intercepts the authorization code (via a malicious browser extension, a compromised redirect, or a custom URI scheme collision on mobile) can exchange it for tokens. PKCE binds the token exchange to the original client by requiring proof that the entity exchanging the code is the same entity that initiated the flow.

The code_verifier is a cryptographically random string (43-128 characters, unreserved URI characters). The code_challenge is its SHA-256 hash, Base64-URL-encoded. The authorization server stores the challenge during the authorize request and verifies the verifier during the token exchange. An attacker with only the authorization code cannot produce the correct verifier.

The State Parameter

The state parameter prevents CSRF attacks against the redirect URI. Generate a cryptographically random value, store it before redirecting to the authorization server, and verify it matches when the redirect returns. If it does not match, reject the response. This is not optional. Every authorization request must include a unique, unpredictable state value.

Token Architecture

An OIDC-based authorization flow may issue up to three token types. The ID token is OIDC's contribution; access and refresh tokens come from OAuth 2.0. Refresh tokens are optional and not always issued.

TokenFormatTypical LifetimeAudienceStorage Location
ID tokenJWT (signed, optionally encrypted)5-60 minutesClient applicationMemory only
Access tokenOpaque string or JWT15-60 minutesResource server (API)Memory; secure storage if needed
Refresh tokenOpaque string (usually; some providers use JWT)Days to weeks (with rotation)Authorization server onlySecure persistent storage

ID Token Anatomy

The ID token is a JWT with three Base64-URL-encoded segments: header, payload, signature. The payload contains claims about the authentication event and the user.

ClaimRequiredDescription
issYesIssuer identifier (authorization server URL)
subYesSubject identifier (unique user ID at this issuer)
audYesAudience (your client_id; reject if it does not match)
expYesExpiration time (Unix timestamp)
iatYesIssued-at time (Unix timestamp)
nonceConditionalReplay protection; required in implicit and hybrid flows, recommended in code flow
auth_timeOptionalTime of original authentication
emailOptionalUser's email (requires email scope)
nameOptionalUser's display name (requires profile scope)

Validate every ID token before trusting it. Verify the signature against the issuer's JWKS (JSON Web Key Set), rejecting alg=none and any unexpected signing algorithms. Check that iss matches your configured issuer. Check that aud contains your client_id. Check that exp is in the future. Check that iat is not unreasonably far in the past. If you sent a nonce in the authorize request, verify it matches. If multiple audiences are present, check the azp (authorized party) claim. Skip any of these checks and you open the door to token substitution attacks.

Access Token Lifecycle

Access tokens are deliberately short-lived. Fifteen to sixty minutes is standard. When an access token expires, the client uses the refresh token to obtain a new one without requiring user interaction. The resource server validates the access token on every request, either by introspecting it against the authorization server or (if the token is a JWT) by verifying the signature and claims locally.

Refresh Token Rotation

Refresh tokens are the most dangerous artifact in the system. A stolen refresh token grants persistent access until it expires or is revoked. Rotation mitigates this: every time a client exchanges a refresh token for a new access token, the authorization server also issues a new refresh token and invalidates the old one.

access_token_1 (15 min) + refresh_token_1POST /token (grant_type=refresh_token, refresh_token_1)Invalidate refresh_token_1access_token_2 (15 min) + refresh_token_2POST /token (grant_type=refresh_token, refresh_token_2)Invalidate refresh_token_2access_token_3 (15 min) + refresh_token_3POST /token (grant_type=refresh_token, refresh_token_1)Detect reuse of invalidated tokenRevoke entire token family401 UnauthorizedAccess token expiresAccess token expires againAttacker tries to use stolen refresh_token_1ClientAuthorization Server
Token lifecycle with refresh rotation

The critical property: if an attacker uses a previously rotated refresh token, the authorization server detects the reuse, revokes the entire token family (all refresh tokens issued to that session), and forces re-authentication. This turns a stolen refresh token from a persistent compromise into a detectable breach.

Configure a grace period of 15-30 seconds to handle network retries. If a client legitimately sends the same refresh token twice (due to a timeout and retry), the second request should succeed within the grace window. After the grace period, treat reuse as a theft signal.

Identity Provider Internals

An OIDC-compliant identity provider (IdP) exposes a standard set of endpoints and publishes its configuration via a discovery document.

Discovery Document

Every OIDC provider publishes a JSON document at /.well-known/openid-configuration containing all endpoint URLs, supported scopes, supported grant types, signing algorithms, and token endpoint authentication methods. Clients should fetch this document at startup and cache it (with reasonable TTL) rather than hardcoding endpoint URLs.

Standard Endpoints

EndpointPurposeMethod
/authorizeInitiate authentication; return authorization codeGET (browser redirect)
/tokenExchange code for tokens; refresh tokensPOST
/userinfoRetrieve user claims with access tokenGET/POST
/revokeRevoke a refresh or access tokenPOST
/introspectValidate token and retrieve metadata (server-side)POST
jwks_uri (from discovery)Public signing keys for JWT verificationGET

Scopes and Claims

Scopes control which claims appear in the ID token and the UserInfo response.

ScopeClaims Returned
openidsub (required for all OIDC requests)
profilename, family_name, given_name, picture, locale, updated_at
emailemail, email_verified
addressaddress (structured JSON)
phonephone_number, phone_number_verified

Request only the scopes you need. Every additional scope increases the token size, expands the consent screen, and broadens the blast radius if tokens are compromised.

Multi-Platform Implementation Patterns

The authorization code flow is the same across all platforms. The redirect mechanism is not. Each platform has a different way to receive the authorization code callback, a different place to store tokens, and a different set of security constraints.

Web BrowserElectron DesktopiOS NativecodecodecodeAuthorizationServerHTTPS redirect URIe.g. https://app.com/callbackToken storage:Memory or httpOnly cookieCustom protocol handlere.g. myapp://auth/callbackOR loopback redirecthttp://127.0.0.1:PORT/callbackToken storage:safeStorage + encrypted fileASWebAuthenticationSessionwith private-use URI schemee.g. com.myapp://callbackToken storage:Keychain Services
Platform-specific redirect and storage patterns

Web Browser: HTTPS Redirect

The simplest pattern. Register an HTTPS redirect URI (e.g., https://app.example.com/auth/callback). After authentication, the browser redirects to your callback URL with the authorization code as a query parameter.

Token storage: For server-rendered apps, exchange the code server-side and store tokens in an encrypted, httpOnly, secure, SameSite cookie. The token never touches JavaScript. For SPAs, store tokens in memory (a module-scoped variable). Do not use localStorage or sessionStorage for tokens. Both are accessible to any JavaScript on the page, which means any XSS vulnerability becomes a token theft vulnerability.

Session management: The server-side session (backed by the cookie) controls the user's authenticated state. When the access token expires, the server uses the refresh token to obtain a new one transparently. The user never sees a re-authentication prompt unless the refresh token itself expires.

Electron Desktop: Custom Protocol or Loopback Redirect

Desktop apps cannot receive HTTPS redirects because they do not run behind a web server with a registered domain. Two patterns work:

Custom protocol handler: Register a protocol scheme (e.g., myapp://) with the operating system. After authentication in the system browser, the authorization server redirects to myapp://auth/callback?code=xxx. On macOS, Electron receives this via the open-url event; on Windows and Linux, the URL arrives through command-line arguments on a second instance. This is cleaner but requires protocol registration during installation.

Loopback redirect: Start a temporary HTTP server on 127.0.0.1 with a random available port. Use http://127.0.0.1:{port}/callback as the redirect URI. After authentication, the browser redirects to localhost, the temporary server captures the code, and shuts down. RFC 8252 explicitly endorses this pattern for native apps. The random port prevents other local applications from intercepting the callback.

Token storage: Use Electron's safeStorage API. It encrypts data using the operating system's credential store (Keychain on macOS, DPAPI on Windows, libsecret/kwallet on Linux). Store the encrypted blob in a file via electron-store or Node's fs module. Never store tokens in plain text on disk.

// Encrypt and store
const encrypted = safeStorage.encryptString(JSON.stringify(tokens));
fs.writeFileSync(tokenPath, encrypted);

// Retrieve and decrypt
const encrypted = fs.readFileSync(tokenPath);
const tokens = JSON.parse(safeStorage.decryptString(encrypted));

iOS Native: ASWebAuthenticationSession + Keychain

iOS uses ASWebAuthenticationSession from the AuthenticationServices framework. It presents a system-managed browser sheet (sharing cookies with Safari), handles the redirect via a private-use URI scheme (e.g., com.example.myapp://callback), and returns the callback URL to your app.

Why ASWebAuthenticationSession: Apple strongly recommends it. Using WKWebView for OAuth is an App Store rejection risk because embedded web views allow the app to intercept credentials. SFSafariViewController was acceptable in earlier iOS versions but ASWebAuthenticationSession (introduced in iOS 12) is the current standard. It provides a browser session the app cannot inspect or modify, which is exactly the security property OAuth requires.

PKCE is mandatory on iOS. The authorization code travels through inter-process communication (from the browser sheet to your app). Without PKCE, a malicious app that has registered the same URI scheme could intercept the code. With PKCE, the intercepted code is useless without the verifier stored in your app's process memory.

Token storage: Use Keychain Services. The iOS Keychain provides hardware-backed encryption, access control with biometric authentication (Face ID/Touch ID), and data protection classes that control when items are accessible (e.g., only when the device is unlocked).

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "refresh_token",
    kSecAttrService as String: "com.example.myapp",
    kSecValueData as String: tokenData,
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)

The kSecAttrAccessibleWhenUnlockedThisDeviceOnly attribute ensures the token is encrypted at rest, accessible only when the device is unlocked, and excluded from backups and device migration. This is a strong default for refresh tokens; apps that need background refresh or extension sharing may require a different accessibility class.

Platform Comparison

ConcernWebElectroniOS
Redirect mechanismHTTPS URLCustom protocol or loopbackASWebAuthenticationSession
Browser usedSame tab or popupSystem default browserSystem browser sheet
Token storagehttpOnly cookie or memorysafeStorage (OS encryption)Keychain Services
Refresh token locationServer-side (cookie session)Encrypted local fileKeychain
MFA supportNative (browser handles it)Native (system browser)Native (browser sheet)
Biometric gateN/AOS-level (optional)Face ID / Touch ID (configurable)

Session Management Architecture

Token expiration and session lifetime are not the same thing. A session outlives individual access tokens via refresh token renewal. The session ends when the refresh token expires, is revoked, or the user explicitly logs out.

Session Lifecycle

  1. Establishment: User completes the OIDC flow. Client receives tokens. Session begins.
  2. Maintenance: Access token expires every 15-60 minutes. Client uses refresh token to obtain a new access token silently. User sees no interruption.
  3. Termination: Refresh token expires (after hours or days, depending on policy), user clicks logout, or the server revokes the token family. Client clears all stored tokens and redirects to login.

Logout Patterns

OIDC defines several logout mechanisms:

PatternHow It WorksUse Case
RP-initiated logoutClient redirects to IdP's end_session_endpoint with id_token_hintStandard logout
Front-channel logoutIdP renders hidden iframes to each client's logout URLMulti-app SSO logout
Back-channel logoutIdP sends POST to each client's registered logout endpointServer-side session cleanup

For multi-application environments with SSO, back-channel logout is the most reliable. Front-channel logout depends on the browser loading iframes, which ad blockers and privacy settings can break. Back-channel logout is a server-to-server call that bypasses the browser entirely.

Silent Token Renewal

Web applications can use hidden iframes to perform silent token renewal via prompt=none on the authorize endpoint. The authorization server checks the existing session (via its own cookie), issues a new authorization code without showing a login prompt, and the iframe captures the callback. This pattern fails when third-party cookies are blocked (Safari's ITP, Firefox's Enhanced Tracking Protection), which makes it unreliable in modern browsers. The recommended alternative is to use refresh tokens with rotation.

Security Threat Model

Every production OAuth deployment faces the same attack surface. The specifics vary by implementation, but the threat categories are consistent.

AttackVectorMitigation
Authorization code interceptionMalicious browser extension, compromised redirect, URI scheme collisionPKCE (binds code to verifier)
Token theft via XSSJavaScript access to tokens in localStorage or sessionStorageStore in httpOnly cookies or memory only
CSRF against redirect URIAttacker initiates flow with victim's sessionState parameter (unique, unpredictable)
Redirect URI manipulationOpen redirect via path traversal or parameter pollutionExact string matching; no wildcards
Refresh token theftCompromised device storage, insecure local storageRotation + reuse detection; encrypted storage
Token substitutionAttacker swaps their ID token into victim's sessionValidate aud, iss, sub on every ID token
ClickjackingEmbedding authorization page in iframeX-Frame-Options / CSP frame-ancestors on authorize endpoint
PKCE downgradeAttacker strips code_challenge from authorize requestServer rejects requests without code_challenge (OAuth 2.1 mandate)

Redirect URI Security

Redirect URI validation is the single most exploited weakness in OAuth deployments. The rules are simple and nonnegotiable:

  1. Register exact redirect URIs at the authorization server. No wildcards. No pattern matching.
  2. The authorization server must compare the redirect_uri parameter using exact string matching against the registered URIs.
  3. Do not allow subdirectory matching (e.g., registering https://app.com/ should not permit https://app.com/evil/path).
  4. Always include the redirect_uri parameter in the token exchange request so the server can verify it matches the original authorization request.

Research published at ACM CCS 2023 demonstrated that the redirect URI validation guidance in OAuth RFCs is under-specified. Multiple identity providers were found vulnerable to path confusion and parameter pollution attacks because they implemented "starts with" matching rather than exact matching. OAuth 2.1 closes this by requiring exact string comparison.

Identity Provider Selection

Choosing an identity provider is an architectural decision with long-term cost, operational, and security implications.

ProviderTypeFree TierPricing ModelBest For
AWS CognitoManaged SaaS10,000 MAUPay per MAU ($0.0055/MAU after free tier)AWS-native workloads
Auth0Managed SaaS25,000 MAUTiered plans ($35-$150+/mo at entry)Developer experience, quick integration
OktaManaged SaaSTrial only$2-6/user/monthEnterprise, compliance-heavy
KeycloakSelf-hosted OSSUnlimited (self-hosted)Infrastructure cost onlyFull control, no vendor lock-in
Microsoft Entra IDManaged SaaS50,000 MAUFree for basic; premium tiers availableMicrosoft ecosystem, B2C

Decision Criteria

Choose AWS Cognito when your infrastructure is already on AWS, your MAU is under 10,000 (free), and you need tight integration with API Gateway, ALB, and CloudFront. The developer experience is rough compared to Auth0, and advanced customization requires Lambda triggers. See AWS Cognito User Authentication: An Architecture Deep-Dive for detailed Cognito architecture.

Choose Auth0 when developer velocity matters more than cost optimization, you need extensive social login support, and your team wants comprehensive SDKs for every platform. Auth0's management dashboard and Rules/Actions pipeline is the best in class for customization.

Choose Keycloak when you need full control over the identity infrastructure, your compliance requirements prohibit sending authentication data to a third-party SaaS, or you want to avoid per-MAU pricing at scale. Running Keycloak reliably requires Kubernetes operational expertise and ongoing patching commitment.

Choose Okta when you are in a regulated industry (healthcare, finance, government) and need SOC 2 Type II, FedRAMP, or HIPAA BAA from your identity provider. Okta's compliance certification portfolio is the deepest of any managed provider.

Key Patterns

PKCE everywhere. No exceptions. Web apps, mobile apps, desktop apps, confidential clients, public clients. OAuth 2.1 mandates it and there is no reason to skip it on any client type.

Short access tokens, rotated refresh tokens. Set access token lifetime to 15-30 minutes. Enable refresh token rotation with reuse detection. Configure a 15-30 second grace period for network retries.

Platform-appropriate token storage. httpOnly cookies for web, safeStorage for Electron, Keychain for iOS. Never localStorage. Never plain text on disk.

Validate everything. Every ID token: signature, issuer, audience, expiration. Every state parameter. Every redirect URI via exact string matching. The security of the entire system depends on validation at each step.

Use the discovery document. Fetch /.well-known/openid-configuration at startup. Cache it. Let the identity provider tell you its endpoints and capabilities rather than hardcoding them.

Build to OAuth 2.1. The draft is stable. The requirements are correct. Every major provider already enforces them. Do not build new systems against OAuth 2.0's permissive defaults.

Additional Resources

Let's Build Something!

I help teams ship cloud infrastructure that actually works at scale. Whether you're modernizing a legacy platform, designing a multi-region architecture from scratch, or figuring out how AI fits into your engineering workflow, I've seen your problem before. Let me help.

Currently taking on select consulting engagements through Vantalect.