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.

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.
| Layer | Specification | Purpose | Key Artifact |
|---|---|---|---|
| Authorization | OAuth 2.0 (RFC 6749) | Delegated access to resources | Access token |
| Identity | OpenID Connect Core 1.0 | Authentication and identity claims | ID token (JWT) |
| Consolidation | OAuth 2.1 (draft-ietf-oauth-v2-1) | Security hardening of OAuth 2.0 | Same 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 Feature | Reason | Replacement |
|---|---|---|
Implicit grant (response_type=token) | Tokens exposed in URL fragments, browser history, server logs | Authorization Code + PKCE |
| Resource Owner Password Credentials (ROPC) | Client handles raw credentials; no MFA support | Authorization Code + PKCE |
| Bearer tokens in query strings | Tokens logged in server access logs, proxy logs, referrer headers | Authorization header only |
| Loose redirect URI matching | Enables open redirect attacks, path confusion, parameter pollution | Exact 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.
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.
| Token | Format | Typical Lifetime | Audience | Storage Location |
|---|---|---|---|---|
| ID token | JWT (signed, optionally encrypted) | 5-60 minutes | Client application | Memory only |
| Access token | Opaque string or JWT | 15-60 minutes | Resource server (API) | Memory; secure storage if needed |
| Refresh token | Opaque string (usually; some providers use JWT) | Days to weeks (with rotation) | Authorization server only | Secure 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.
| Claim | Required | Description |
|---|---|---|
iss | Yes | Issuer identifier (authorization server URL) |
sub | Yes | Subject identifier (unique user ID at this issuer) |
aud | Yes | Audience (your client_id; reject if it does not match) |
exp | Yes | Expiration time (Unix timestamp) |
iat | Yes | Issued-at time (Unix timestamp) |
nonce | Conditional | Replay protection; required in implicit and hybrid flows, recommended in code flow |
auth_time | Optional | Time of original authentication |
email | Optional | User's email (requires email scope) |
name | Optional | User'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.
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
| Endpoint | Purpose | Method |
|---|---|---|
/authorize | Initiate authentication; return authorization code | GET (browser redirect) |
/token | Exchange code for tokens; refresh tokens | POST |
/userinfo | Retrieve user claims with access token | GET/POST |
/revoke | Revoke a refresh or access token | POST |
/introspect | Validate token and retrieve metadata (server-side) | POST |
jwks_uri (from discovery) | Public signing keys for JWT verification | GET |
Scopes and Claims
Scopes control which claims appear in the ID token and the UserInfo response.
| Scope | Claims Returned |
|---|---|
openid | sub (required for all OIDC requests) |
profile | name, family_name, given_name, picture, locale, updated_at |
email | email, email_verified |
address | address (structured JSON) |
phone | phone_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 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
| Concern | Web | Electron | iOS |
|---|---|---|---|
| Redirect mechanism | HTTPS URL | Custom protocol or loopback | ASWebAuthenticationSession |
| Browser used | Same tab or popup | System default browser | System browser sheet |
| Token storage | httpOnly cookie or memory | safeStorage (OS encryption) | Keychain Services |
| Refresh token location | Server-side (cookie session) | Encrypted local file | Keychain |
| MFA support | Native (browser handles it) | Native (system browser) | Native (browser sheet) |
| Biometric gate | N/A | OS-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
- Establishment: User completes the OIDC flow. Client receives tokens. Session begins.
- Maintenance: Access token expires every 15-60 minutes. Client uses refresh token to obtain a new access token silently. User sees no interruption.
- 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:
| Pattern | How It Works | Use Case |
|---|---|---|
| RP-initiated logout | Client redirects to IdP's end_session_endpoint with id_token_hint | Standard logout |
| Front-channel logout | IdP renders hidden iframes to each client's logout URL | Multi-app SSO logout |
| Back-channel logout | IdP sends POST to each client's registered logout endpoint | Server-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.
| Attack | Vector | Mitigation |
|---|---|---|
| Authorization code interception | Malicious browser extension, compromised redirect, URI scheme collision | PKCE (binds code to verifier) |
| Token theft via XSS | JavaScript access to tokens in localStorage or sessionStorage | Store in httpOnly cookies or memory only |
| CSRF against redirect URI | Attacker initiates flow with victim's session | State parameter (unique, unpredictable) |
| Redirect URI manipulation | Open redirect via path traversal or parameter pollution | Exact string matching; no wildcards |
| Refresh token theft | Compromised device storage, insecure local storage | Rotation + reuse detection; encrypted storage |
| Token substitution | Attacker swaps their ID token into victim's session | Validate aud, iss, sub on every ID token |
| Clickjacking | Embedding authorization page in iframe | X-Frame-Options / CSP frame-ancestors on authorize endpoint |
| PKCE downgrade | Attacker strips code_challenge from authorize request | Server 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:
- Register exact redirect URIs at the authorization server. No wildcards. No pattern matching.
- The authorization server must compare the
redirect_uriparameter using exact string matching against the registered URIs. - Do not allow subdirectory matching (e.g., registering
https://app.com/should not permithttps://app.com/evil/path). - Always include the
redirect_uriparameter 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.
| Provider | Type | Free Tier | Pricing Model | Best For |
|---|---|---|---|---|
| AWS Cognito | Managed SaaS | 10,000 MAU | Pay per MAU ($0.0055/MAU after free tier) | AWS-native workloads |
| Auth0 | Managed SaaS | 25,000 MAU | Tiered plans ($35-$150+/mo at entry) | Developer experience, quick integration |
| Okta | Managed SaaS | Trial only | $2-6/user/month | Enterprise, compliance-heavy |
| Keycloak | Self-hosted OSS | Unlimited (self-hosted) | Infrastructure cost only | Full control, no vendor lock-in |
| Microsoft Entra ID | Managed SaaS | 50,000 MAU | Free for basic; premium tiers available | Microsoft 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
- OAuth 2.1 Draft Specification (draft-ietf-oauth-v2-1-15)
- OpenID Connect Core 1.0 Specification
- RFC 7636: Proof Key for Code Exchange (PKCE)
- RFC 8252: OAuth 2.0 for Native Applications
- RFC 6819: OAuth 2.0 Threat Model and Security Considerations
- Auth0 Token Best Practices
- Okta Token Lifecycle Reference
- Apple ASWebAuthenticationSession Documentation
- Electron safeStorage API Documentation
- OAuth 2.0 Security Best Current Practice (draft-ietf-oauth-security-topics)
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.

