OAuth2 is an authorization framework that lets users grant third-party applications limited access to their data without sharing passwords. OpenID Connect (OIDC) is an identity layer on top of OAuth2 that also authenticates who the user is. Together, they power “Login with Google,” GitHub OAuth integrations, and enterprise SSO. Understanding how they work is essential for any system that handles user authentication or API authorization.
OAuth2 Authorization Code Flow
The most secure and common OAuth2 flow for server-side applications: (1) User clicks “Login with GitHub” on your app. (2) Your app redirects the user to GitHub’s authorization endpoint with parameters: client_id (your app’s ID), redirect_uri (where GitHub sends the user back), scope (what permissions you’re requesting: “read:user email”), state (random CSRF token your app generates), response_type=code. (3) GitHub shows a consent screen. User approves. (4) GitHub redirects back to your redirect_uri with an authorization code and the state parameter. Verify state matches — prevents CSRF. (5) Your backend server exchanges the code for tokens: POST to GitHub’s token endpoint with code, client_id, client_secret, redirect_uri. GitHub returns: access_token (short-lived, for API calls), refresh_token (long-lived, for getting new access tokens), id_token (OIDC — JWT containing user identity). (6) Your app stores the tokens and uses the access_token for API calls on behalf of the user. The authorization code is single-use and short-lived (10 minutes) — prevents replay attacks if the redirect URL is intercepted.
PKCE: Proof Key for Code Exchange
PKCE (Proof Key for Code Exchange) is required for public clients — single-page apps and mobile apps that can’t securely store a client_secret. Without PKCE, an attacker who intercepts the authorization code can exchange it for tokens (they don’t need the client_secret for public clients). PKCE flow: (1) The app generates a cryptographically random code_verifier (43-128 characters). (2) Computes code_challenge = BASE64URL(SHA256(code_verifier)). (3) Includes code_challenge and code_challenge_method=S256 in the authorization request. (4) GitHub stores the code_challenge. (5) When exchanging the code for tokens, the app sends the code_verifier. (6) GitHub computes SHA256(code_verifier) and verifies it matches the stored code_challenge. An intercepted authorization code is useless without the code_verifier, which never leaves the app. PKCE is now recommended for all OAuth2 clients, including server-side apps.
OpenID Connect and the ID Token
OAuth2 answers “what can this app access?”. OIDC additionally answers “who is the user?”. The id_token is a JWT returned by the authorization server containing: sub (subject — a unique user identifier in this provider’s system), name, email, picture, iss (issuer — the authorization server’s URL), aud (audience — your client_id), exp (expiry), iat (issued at). Validate the id_token: verify the JWT signature using the provider’s public keys (fetched from their JWKS endpoint), verify iss matches the expected provider, verify aud matches your client_id, verify exp hasn’t passed. After validation, the sub claim uniquely identifies the user. Store the association: your user table maps (provider, sub) → your internal user_id. If a user logs in with Google for the first time (unknown sub), create an account. If known sub, log them in. Never use the email from the id_token as the unique identifier — email addresses can change and may be shared across providers.
Token Storage and Security
Where to store OAuth2 tokens in a browser: (1) HttpOnly cookies: the access_token and refresh_token are stored in HttpOnly cookies by your backend. JavaScript cannot read them — XSS cannot steal tokens. The backend acts as a BFF (Backend for Frontend), adding the token to API calls server-side. Most secure for web apps. (2) localStorage: accessible to JavaScript — XSS vulnerability can steal tokens. Avoid for long-lived tokens. (3) In-memory (JavaScript variable): lost on page refresh but not accessible to XSS from other origins. Acceptable for short-lived access tokens; pair with HttpOnly refresh tokens. Token refresh: when the access_token expires (typically 1 hour), use the refresh_token to get a new access_token from the provider. The refresh_token rotation: each use of a refresh_token invalidates the old one and returns a new one — if a refresh_token is stolen and used by an attacker, the legitimate user’s next refresh attempt fails (detecting the theft).
Authorization Server Design
Building your own OAuth2 authorization server (for your own API clients or enterprise SSO): Client registry: store registered applications (client_id, client_secret hash, allowed redirect_uris, allowed scopes, client_type). Authorization code store: temporary store (Redis with short TTL) mapping code → (client_id, user_id, scope, code_challenge, expiry). On token exchange: look up the code, validate, delete it (single use), issue tokens. Token store: if using opaque access tokens, store them in Redis with the associated user_id, scope, and expiry. For JWT access tokens, no server-side store is needed — validate the signature. Scope enforcement: when a protected API receives a request, validate the access_token’s scope includes the required permission. A token with scope=”read” cannot access write endpoints. Implement as middleware that extracts and validates the token before routing to the business logic.