Guide

OAuth 2.0 and OpenID Connect explained

"Sign in with Google" is everywhere — but most developers confuse what OAuth actually does. OAuth 2.0 is an authorization framework: it lets a user grant a third-party application limited access to resources on another service, without handing over their password. OpenID Connect (OIDC) layers on top of OAuth to answer a different question: who is this user? — adding standardized identity tokens and a discovery document so clients do not reinvent login. Mixing the two, skipping PKCE on public clients, or treating OAuth scopes as your entire permission model are how production breaches start. This guide walks through the roles (resource owner, client, authorization server, resource server), the flows you should actually use in 2026, how access tokens differ from ID tokens, scope design, refresh-token rotation, redirect URI pitfalls, and a checklist before you ship "Login with X" to real users.

Authorization vs authentication — and where OIDC fits

OAuth's core job is delegated authorization. A photo-printing app should be able to read albums from a cloud drive because the user clicked "Allow" — not because the app stores the user's Google password. The authentication vs authorization distinction matters: OAuth issues tokens that prove the user (or client) was granted certain access rights. It does not, by itself, standardize how you learn the user's display name or email.

OpenID Connect fixes that gap. Built on OAuth 2.0, OIDC adds an ID token (a signed JWT with claims like sub, email, and name), a userinfo endpoint, and a /.well-known/openid-configuration discovery URL so clients auto-configure endpoints. When product copy says "Sign in with GitHub," you are almost always running an OIDC-flavored authorization code flow, not "pure" resource delegation.

The four roles in every OAuth deployment

Every OAuth conversation involves four parties. Naming them correctly prevents architecture mistakes:

  • Resource owner — the human (or organization) who can grant access. Usually the logged-in user at the identity provider.
  • Client — the application requesting access: your SPA, mobile app, or backend service.
  • Authorization server — issues tokens after authenticating the user and recording consent. Examples: Auth0, Okta, Keycloak, Cognito, or Google Accounts.
  • Resource server — the API that holds protected data (Google Drive, GitHub repos, your own REST API). It validates access tokens on each request.

In many SaaS setups the authorization server and resource server are the same vendor (Google issues tokens and hosts Gmail). In microservice architectures they split: your auth service mints tokens; individual services validate them at the API gateway or via shared JWKS keys.

OAuth flows — which one to use

OAuth 2.1 (the consolidated best-practice spec) narrows the menu. Deprecated patterns (implicit flow, password grant for third parties) should not appear in new systems. The decision table below reflects what security reviewers expect in 2026:

Authorization code + PKCE (default for user login)

The browser or app redirects the user to the authorization server, the user signs in and consents, and the server redirects back with a one-time code. The client exchanges that code at the token endpoint for access (and optionally refresh) tokens. PKCE (Proof Key for Code Exchange) adds a code_verifier / code_challenge pair so intercepted authorization codes cannot be redeemed by an attacker — mandatory for public clients (SPAs, mobile) and recommended everywhere.

  1. Client generates random code_verifier, hashes to code_challenge.
  2. Redirect user to /authorize with response_type=code, client_id, redirect_uri, scope, state, and PKCE challenge.
  3. User authenticates; authorization server redirects to redirect_uri?code=...&state=...
  4. Client verifies state matches (CSRF protection), POSTs to /token with code, code_verifier, and client authentication if confidential.
  5. Authorization server returns access token (+ refresh token, ID token if OIDC).

Client credentials (machine-to-machine)

No user in the loop. A confidential backend client authenticates with client_id + client_secret (or mTLS / private-key JWT) and receives an access token scoped to service permissions. Use for cron jobs, ETL pipelines, and internal microservices — never embed the secret in a mobile app or browser bundle. Store credentials in a secrets manager, rotate regularly, and scope tokens to the minimum API surface.

Device authorization grant (TVs and CLI tools)

For input-constrained devices: the client displays a user code and URL; the user completes login on a phone or laptop. Polling the token endpoint returns tokens once the user approves. Essential for smart TVs, IoT panels, and developer CLIs where embedding a full browser OAuth redirect is awkward.

Flows to avoid

  • Implicit flow — tokens returned in the URL fragment; leaked via referrer headers and browser history. Removed from OAuth 2.1.
  • Resource owner password credentials — your app collects the user's IdP password. Only acceptable for first-party migration; never for third-party integrations.
  • Authorization code without PKCE on SPAs — treat as a vulnerability waiting for a malicious redirect handler.

Access tokens vs ID tokens

Teams routinely misuse token types. Keep the contract straight:

  • Access token — presented to resource servers in the Authorization: Bearer header. Proves the holder may perform actions within granted scopes until expiry. May be opaque or a JWT; validation is the resource server's job (signature, issuer, audience, expiry, scope).
  • ID token — OIDC only; intended for the client to learn who authenticated. Contains identity claims (sub, name, email). Never send an ID token to your product API as if it were an access token — APIs should reject tokens with wrong aud (audience) or missing resource indicators.
  • Refresh token — long-lived secret used only at the token endpoint to obtain new access tokens without re-prompting the user. Treat as highly sensitive; bind to client, rotate on each use, and revoke on logout.

A SPA that stores access tokens in localStorage is vulnerable to XSS exfiltration. Prefer backend-for-frontend (BFF) patterns: the browser holds an HttpOnly session cookie; the BFF holds refresh tokens server-side and attaches short-lived access tokens to upstream calls. Pair with strict Content Security Policy — see CSRF and XSS defenses.

Scopes — consent boundaries, not your entire AuthZ model

Scopes are strings the user approves on the consent screen: openid profile email, read:orders, repo:write. They define what the token may request from the authorization server's APIs — not automatically what your application's internal roles allow.

Good scope design is coarse at the OAuth layer, fine inside your app:

  • Request the minimum scopes at login — add incremental consent when the user triggers a feature that needs more (e.g. calendar write only when they enable scheduling).
  • Document each scope in your OpenAPI spec if you expose third-party developer APIs.
  • Enforce row-level authorization in your service even when the token carries admin — stolen tokens should have bounded blast radius.
  • Standard OIDC scopes: openid (required for ID token), profile, email, address, phone. Custom API scopes should be namespaced (https://api.example.com/orders.read) to avoid collisions.

Redirect URIs, state, and common misconfigurations

Redirect URI validation is the top OAuth misconfiguration in breach reports. Attackers register lookalike apps or exploit loose matching to steal authorization codes.

  • Exact match only — register full URIs; no wildcard subdomains unless you fully control the parent domain and understand the risk.
  • HTTPS everywhere — except http://127.0.0.1 for local dev with fixed ports. Never http:// production callbacks.
  • Validate state — cryptographically random, stored server-side or in sealed cookie; reject callbacks with missing or wrong state to block login CSRF.
  • Validate nonce (OIDC) — bind ID token to the original auth request; reject replayed ID tokens.
  • Check iss (issuer) — accept tokens only from your configured authorization server; prevents token substitution from a rogue IdP.
  • Audience (aud) — access tokens must list your API as intended recipient; ID tokens list your client_id.
  • Clock skew — allow small tolerance on exp / nbf but reject expired tokens; sync NTP on validators.

Multi-tenant SaaS adds complexity: each customer might bring their own IdP (enterprise SAML/OIDC federation). Centralize issuer allowlists, map external sub claims to internal user IDs, and never trust email alone as a stable identifier — it can change or be unverified.

Token lifecycle — refresh, revocation, and logout

Short-lived access tokens (5–15 minutes) limit damage from leakage. Refresh tokens enable silent renewal but require discipline:

  • Rotation — each refresh returns a new refresh token and invalidates the old one; detect reuse (possible theft) and revoke the whole family.
  • Revocation endpoint — call on explicit logout; clear client storage and server-side sessions.
  • Back-channel logout (OIDC) — IdP notifies clients when the user signs out globally; implement if your threat model includes shared computers.
  • Session management — "logout" in your app must mean more than deleting a cookie; invalidate refresh tokens at the authorization server.

For high-assurance apps (payments, key export), combine OAuth with step-up authentication — require fresh MFA even when the access token is valid.

Choosing and operating an identity provider

Build vs buy is a real tradeoff. Managed IdPs (Auth0, Clerk, Cognito, Firebase Auth, Azure AD B2C) ship OIDC discovery, consent UI, breach monitoring, and compliance artifacts. Self-hosted Keycloak or Ory gives control and data residency at the cost of patching, scaling, and key ceremony.

Evaluation criteria:

  • OIDC compliance and PKCE support out of the box
  • Social and enterprise federation (SAML/OIDC upstream)
  • Custom claim mapping and rules for multi-tenant apps
  • Rate limits and SLA on the token endpoint (your login depends on it)
  • Audit logs for consent grants and admin changes
  • Data processing agreements if you handle EU users (GDPR)

Whichever you choose, treat the authorization server as tier-zero infrastructure — downtime means nobody logs in, and a compromise hands attackers the keys to mint valid tokens across your fleet.

Flow selection decision table

Scenario Recommended flow Notes
Web or mobile app — user login Authorization code + PKCE BFF or native secure storage for tokens; avoid implicit
Backend service calling APIs Client credentials Secret in vault; narrow scopes; no user context
Smart TV, printer, CLI Device authorization grant Poll with backoff; short user-code TTL
First-party migration only Password grant (legacy) Plan removal; never for third-party clients
SPA without backend Auth code + PKCE via lightweight BFF Do not store refresh tokens in browser storage long-term

Production checklist

  1. Use authorization code + PKCE for all public clients; disable implicit flow.
  2. Register exact redirect URIs; reject unknown callbacks.
  3. Validate state, nonce (OIDC), iss, aud, and exp on every token.
  4. Keep access tokens short-lived; rotate refresh tokens; revoke on logout.
  5. Separate ID tokens (client identity) from access tokens (API authorization).
  6. Enforce app-level AuthZ after token validation — scopes are not enough alone.
  7. Store client secrets and refresh tokens in a secrets manager, never in git.
  8. Log auth events (login, consent, refresh, revoke) without logging token values.
  9. Document scopes in your API contract; review quarterly for scope creep.
  10. Run annual redirect-URI and client inventory audits; delete unused OAuth apps.

Key takeaways

  • OAuth delegates access; OIDC adds standardized login with ID tokens and discovery.
  • Authorization code + PKCE is the default for user-facing apps in 2026.
  • Access tokens go to APIs; ID tokens stay with the client — do not swap them.
  • Scopes are consent boundaries, not a replacement for your permission model.
  • Redirect URI exactness and token validation prevent the most common real-world OAuth breaches.

Related reading