Understanding OAuth 2.0 and OpenID Connect: A Developer’s Implementation Guide

OAuth 2.0 has been the backbone of delegated authorization on the web for over a decade, and yet it remains one of the most frequently misimplemented specifications in production software. Pair it with OpenID Connect, and you have a powerful but nuanced authentication and authorization stack that rewards careful engineering and punishes shortcuts ruthlessly.

This OAuth 2.0 OpenID Connect implementation guide is written for developers who need to move past the tutorials and build something that holds up under real traffic, real attackers, and real compliance requirements. The goal is practical clarity: which grant type fits your architecture, how tokens actually work, where teams consistently get burned, and what libraries deserve your trust.

OAuth 2.0: The Authorization Layer

OAuth 2.0 is an authorization framework, not an authentication protocol. This distinction matters more than most teams realize. OAuth tells a resource server that a client has been granted specific permissions. It says nothing, by itself, about who the user is. That gap is precisely what OpenID Connect fills, but we will get there shortly.

The framework defines several grant types, each designed for a distinct interaction pattern. Choosing the wrong one introduces friction at best and security vulnerabilities at worst.

Authorization Code Grant

This is the workhorse of OAuth 2.0 and the grant type you should reach for by default when a user is involved. The flow works like this: the client redirects the user to the authorization server, the user authenticates and consents, the authorization server redirects back with a short-lived authorization code, and the client exchanges that code for tokens on the back channel.

The critical property here is that tokens never pass through the browser’s URL bar or front channel. The authorization code itself is useless without the client secret (or PKCE verifier), which limits the damage from interception.

Use when: server-side web applications, single-page applications (with PKCE), mobile applications (with PKCE).

Client Credentials Grant

When there is no user in the picture, client credentials is the correct choice. A service authenticates directly with the authorization server using its own credentials and receives an access token. There is no redirect, no browser, no consent screen.

Use when: service-to-service communication, background jobs, microservice architectures, CLI tools acting on behalf of the application rather than a user.

Device Authorization Grant (Device Flow)

This grant type solves a real UX problem: how do you authenticate on a device with limited input capabilities? Smart TVs, IoT devices, and CLI tools that cannot easily open a browser use the device flow. The device displays a code, the user enters it on a separate device with a full browser, and the original device polls the authorization server until it gets tokens.

Use when: smart TVs, IoT devices, CLI tools where browser redirect is impractical.

Grant Type Comparison

Grant Type User Involved Client Type Token Delivery PKCE Required Typical Use Case
Authorization Code Yes Confidential or Public Back channel Required for public clients, recommended for all Web apps, SPAs, mobile apps
Client Credentials No Confidential Direct response No Service-to-service, background jobs
Device Authorization Yes Public Polling No (separate user agent) Smart TVs, IoT, CLI tools

A note on deprecated flows: The implicit grant and resource owner password credentials grant are effectively dead. The OAuth 2.1 draft removes both. If you are still using either in production, migrating away should be a priority. The implicit grant exposes tokens in URL fragments, and ROPC requires users to hand their passwords to third-party clients, which defeats the entire point of OAuth.

OpenID Connect: The Identity Layer

OpenID Connect (OIDC) sits on top of OAuth 2.0 and answers the question OAuth deliberately left open: who is this user? It introduces the ID token, a JWT that contains claims about the authenticated user, their authentication time, the issuer, and the audience.

OIDC also standardizes a UserInfo endpoint, a discovery mechanism (/.well-known/openid-configuration), and a set of scopes (openid, profile, email) that make identity data interoperable across providers.

The relationship between OAuth 2.0 and OIDC confuses many developers. Think of it this way: OAuth 2.0 gives your application a key to a specific room. OIDC tells your application who handed over the key. You almost always want both.

The ID Token

The ID token is a signed JWT with a well-defined structure. The claims you should validate on every authentication:

  • iss (issuer): Must match the expected authorization server
  • aud (audience): Must contain your client ID
  • exp (expiration): Must not be in the past
  • iat (issued at): Useful for freshness checks
  • nonce: Must match the nonce you sent in the authorization request (prevents replay attacks)
  • sub (subject): The stable user identifier

Skipping any of these validations is a security defect. I have reviewed production codebases that validate the signature but ignore the audience claim entirely, which means any token issued by the same provider for a different application would be accepted.

Token Management in Practice

A working OAuth/OIDC implementation juggles three types of tokens, each with a different purpose and lifecycle.

Access Tokens

Access tokens authorize API requests. They are typically short-lived (5 to 60 minutes) and can be either opaque strings or JWTs. The resource server validates them on every request, either by introspection (calling the authorization server) or local JWT verification.

Keep access token lifetimes short. A 5-minute access token limits the blast radius of a stolen credential. Longer lifetimes are tempting for reducing refresh traffic, but the tradeoff is rarely worth it.

Refresh Tokens

Refresh tokens are long-lived credentials used to obtain new access tokens without user interaction. They are powerful and dangerous. A stolen refresh token gives an attacker persistent access until it is revoked.

Best practices for refresh tokens: store them server-side whenever possible, implement refresh token rotation (every use issues a new refresh token and invalidates the old one), set absolute expiration times, and detect reuse as a signal of compromise.

ID Tokens

ID tokens are for the client application. They assert identity at a point in time. Do not send ID tokens to your API as authorization. That is what access tokens are for. ID tokens should be consumed once at login, validated, and their claims used to establish a session.

Token Exchange Flow: Authorization Code with PKCE

Here is the concrete flow for exchanging an authorization code for tokens, which is the most common pattern you will implement:

// Step 1: Generate PKCE parameters before redirecting the user
const codeVerifier = generateRandomString(64); // cryptographic random
const codeChallenge = base64urlEncode(sha256(codeVerifier));

// Step 2: Build authorization URL
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomString(32));
authUrl.searchParams.set('nonce', generateRandomString(32));
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Store state, nonce, and codeVerifier in session for later validation
// Redirect user to authUrl

// Step 3: Handle the callback - exchange code for tokens
async function handleCallback(code, returnedState) {
  // Validate state parameter matches what was stored in session
  if (returnedState !== storedState) {
    throw new Error('State mismatch - possible CSRF attack');
  }

  const tokenResponse = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: storedCodeVerifier  // PKCE proof
    })
  });

  const tokens = await tokenResponse.json();
  // tokens contains: access_token, refresh_token, id_token, expires_in

  // Step 4: Validate the ID token before trusting its claims
  const idTokenClaims = await verifyIdToken(tokens.id_token, {
    issuer: 'https://auth.example.com',
    audience: CLIENT_ID,
    nonce: storedNonce
  });

  return { accessToken: tokens.access_token, user: idTokenClaims };
}

Every step here matters. Remove the state validation and you are vulnerable to CSRF. Remove PKCE and a public client is vulnerable to authorization code interception. Skip ID token validation and you are trusting unverified claims.

Security Pitfalls That Actually Bite

Security documentation tends toward the exhaustive. Here I want to focus on the mistakes I encounter most frequently in code reviews and security assessments.

PKCE Is Not Optional

Proof Key for Code Exchange was originally designed for mobile and SPA clients that cannot securely store a client secret. The current best practice, and the OAuth 2.1 requirement, is to use PKCE for all authorization code flows, including confidential clients. It costs nothing in complexity and provides defense in depth against code interception attacks.

Use S256 as the challenge method. Plain is allowed by the spec but provides no security benefit. If your authorization server does not support PKCE, that is a significant red flag about its maintenance status.

The State Parameter

The state parameter prevents cross-site request forgery against the redirect URI. Without it, an attacker can craft a URL that causes a victim’s browser to complete an OAuth flow with the attacker’s authorization code, binding the victim’s session to the attacker’s account.

Generate state values using a cryptographically secure random number generator. Bind them to the user’s session. Validate them on every callback. This is not negotiable.

Token Storage

Where you store tokens depends on your client type, and getting it wrong is one of the most common vulnerabilities in OAuth implementations:

  • Server-side applications: Store tokens in an encrypted server-side session. This is the most secure option because tokens never reach the browser.
  • Single-page applications: Keep tokens in memory (JavaScript variables). Avoid localStorage, which is accessible to any script on the same origin, making XSS attacks devastating. If you need persistence across page reloads, consider a Backend-for-Frontend (BFF) pattern that keeps tokens server-side.
  • Mobile applications: Use platform-specific secure storage: Keychain on iOS, EncryptedSharedPreferences on Android. Never store tokens in plain text files or unencrypted databases.

The BFF pattern deserves more adoption than it currently has. By proxying token operations through a thin backend, you get the UX of an SPA with the token security of a server-side application. The added complexity is modest and the security improvement is substantial.

Redirect URI Validation

Authorization servers must perform exact-match validation on redirect URIs. Clients must register the full URI, not just a domain. Open redirect vulnerabilities in OAuth flows let attackers steal authorization codes by manipulating the redirect target. If your provider allows wildcard redirect URIs in production, push back hard.

JWT Verification Done Wrong

A recurring class of vulnerabilities stems from incomplete JWT verification. Common failures include:

  • Not validating the alg header, which enables algorithm confusion attacks where an attacker switches from RS256 to HS256 and signs with the public key
  • Not checking the aud claim, accepting tokens intended for other clients
  • Using the JWT payload before verifying the signature
  • Fetching JWKS (public keys) from an attacker-controlled URL instead of the configured issuer

Use a well-maintained library for JWT verification. Do not implement it yourself. The specification has enough edge cases that hand-rolled implementations almost always have flaws.

Common Implementation Mistakes

Beyond security issues, several architectural mistakes make OAuth implementations fragile or unmaintainable.

Overloading the ID token. Teams frequently stuff custom claims into the ID token and send it to APIs. The ID token is for the client to establish who the user is. Use access tokens with proper scopes for API authorization.

Ignoring token expiration gracefully. When an access token expires, your application needs to transparently refresh it. Many implementations surface a login prompt on the first 401, rather than attempting a refresh. Build token renewal into your HTTP client layer so it is automatic and invisible to the rest of your application code.

Not implementing proper logout. OAuth and OIDC define several logout mechanisms: RP-initiated logout, back-channel logout, and front-channel logout. Many teams implement login perfectly but forget that ending a session requires revoking tokens and notifying the authorization server. Incomplete logout means users cannot effectively sign out, which is both a security and compliance problem.

Hardcoding provider configuration. Use the OIDC discovery endpoint (/.well-known/openid-configuration) to fetch provider metadata dynamically. Hardcoding endpoints, JWKS URIs, and supported scopes means your integration breaks whenever the provider makes changes, and you will not find out until production is on fire.

Library Recommendations

The best security advice for OAuth implementation is to not implement it yourself. Use battle-tested libraries that handle the protocol details, token management, and cryptographic operations.

Server-Side

  • Node.js: openid-client for OIDC relying party operations; jose for JWT/JWK/JWS operations. Both are maintained by Filip Skokan, who is also an editor of several OAuth/OIDC specifications.
  • Python: Authlib provides a comprehensive OAuth/OIDC implementation for both client and server roles. For Django specifically, django-allauth handles the common provider integrations well.
  • Go: coreos/go-oidc for OIDC client operations paired with golang.org/x/oauth2 for the OAuth layer.
  • Java/.NET: Spring Security OAuth2 and Microsoft.Identity.Web respectively. Both are vendor-backed and well-maintained.

Client-Side (SPAs and Mobile)

  • SPAs: oidc-client-ts is a solid, maintained fork of the original oidc-client-js. For React specifically, react-oidc-context wraps it with appropriate hooks.
  • Mobile: AppAuth (AppAuth-iOS, AppAuth-Android) implements current best practices for native applications, including PKCE and custom URI scheme handling.

When evaluating libraries, check for active maintenance, conformance to current specs (OAuth 2.1, FAPI 2.0), and proper PKCE support. A library that was last updated in 2021 is likely missing critical security improvements.

Putting It Together

A well-implemented OAuth 2.0 and OpenID Connect stack is not just a security requirement; it is a foundation that affects user experience, operational complexity, and your ability to integrate with external services. The protocol pair is mature, well-specified, and supported by excellent tooling. The problems almost always come from implementation shortcuts, not from flaws in the specification itself.

Start with the authorization code grant and PKCE. Use OIDC discovery to configure your client. Validate every claim in every token. Store tokens appropriately for your client type. Implement refresh token rotation. Use established libraries and keep them updated.

The teams that treat authentication infrastructure with the same rigor they apply to their database layer or their deployment pipeline are the ones that avoid the late-night incident where a token validation bug exposed user data. OAuth and OIDC reward careful, thorough implementation. Cut corners and they will let you know, usually at the worst possible time.


Michael Sun covers security infrastructure, developer tooling, and systems architecture for NovVista. Reach him on X @sunjianyin.

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *