Skip to main content

Chapter 16: OAuth and OpenID Connect — Authentication Integration With External Services

What you'll learn in this chapter

  • Distinguish OAuth 2.0's four roles (Resource Owner / Client / Authorization Server / Resource Server)
  • Understand that Authorization Code Flow + PKCE is the modern standard
  • Explain the structure of an ID Token (JWT) — header.payload.signature — and its required claims
  • Design a database for social login
  • Understand the background behind Implicit Flow being deprecated
Prerequisites

You should understand Chapter 15: Designing an Authentication and Authorization System. OAuth is a complex specification, and this chapter focuses narrowly on the entry point to implementing it.

What Is OAuth 2.0?

OAuth 2.0 is an authorization protocol for "letting another service use your service's data." Common scenarios:

  • "Log in with your Google account" (Google authenticates → your own site receives that identity)
  • "Post via the Twitter API from your own app" (your app accesses a user's Twitter data on their behalf)
  • "Send GitHub notifications to Slack" (Slack has permission to receive GitHub's webhooks)
Terminology: OAuth and OpenID Connect (OIDC)
  • OAuth 2.0 is a protocol for authorization (does this person have the right to access X?)
  • OpenID Connect (OIDC) is an extension built on top of OAuth 2.0 for authentication (who is this person?)

"Log in with Google" is actually OpenID Connect (= authorization via OAuth 2.0, plus identity verification via an ID Token). The two are easy to conflate, but they address different problems.

OAuth 2.0's Four Roles

RoleExample
Resource Owner (the user)You (the owner of the Google account)
Client (the app)A third-party app you use (a photo-editing app, for example)
Authorization ServerGoogle's login server
Resource ServerThe Google Drive API

"The Client accesses the Resource Owner's data from the Resource Server, having gone through the Authorization Server's authorization" is the basic structure of OAuth.

Authorization Code Flow + PKCE (The Modern Standard)

OAuth 2.0 has several "flows" (authorization procedures), but as of 2026, the standard is Authorization Code Flow + PKCE (RFC 9700 / the OAuth 2.1 draft).

The OAuth security advisory RFC 9700 (2025-01) makes PKCE (Proof Key for Code Exchange) mandatory (MUST) for a Public Client (an SPA or mobile app). It's recommended (RECOMMENDED) for a Confidential Client (server-side) too, though OpenID Connect's confidential clients can substitute the nonce parameter. Furthermore, the OAuth 2.1 draft builds PKCE directly into the authorization code grant procedure itself, making it a de facto standard feature. PKCE was originally introduced for Public Clients in RFC 7636 (2015).

With PKCE, even if a malicious intermediary steals the authorization code, they can't exchange it for a token without knowing the code_verifier.

Don't Use Implicit Flow

Implicit Flow is deprecated and removed

Implicit Flow, once used for SPAs, was deprecated by RFC 9700 (2025-01) and removed by the OAuth 2.1 draft. Older tutorials or explanations from 2018 or earlier sometimes introduce Implicit Flow, but the correct approach today is to use Authorization Code Flow + PKCE, even for an SPA.

Reason: Implicit Flow passes the token via the URL fragment, which carries a high risk of it leaking through browser history, referrer headers, or a script on the redirect destination.

OpenID Connect's ID Token (JWT)

OpenID Connect issues an ID Token representing "who this person is." The ID Token itself is a standard format called JWT (JSON Web Token, RFC 7519) — a string made of three parts separated by ..

eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTAxNjkxNTM1NTkzMjEzOTI3MzMiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJleHAiOjE3MzAwMDAwMDAsImlhdCI6MTcyOTk5NjQwMH0.xxxxxxxxxxxxxxxxxx
└────── Header ──────┘ └─────────────── Payload ───────────────────────┘ └─ Signature ─┘

The Three Parts

PartContent
HeaderThe algorithm (alg) and key ID (kid)
PayloadClaims (information about the user)
SignatureA cryptographic signature over the header + payload (e.g., RS256)

Required Claims (Defined by OpenID Connect Core 1.0)

ClaimMeaning
iss (issuer)The issuer (e.g., https://accounts.google.com)
sub (subject)The user's unique ID (unique within the issuer)
aud (audience)The intended recipient of this token (the client ID)
exp (expiration)The expiration time (Unix time)
iat (issued at)The time it was issued

Verification Steps (Client Side)

  1. Verify the signature: verify the Signature with the issuer's public key (fetched from /.well-known/openid-configuration)
  2. Is iss the expected issuer?
  3. Is aud your own client ID?
  4. Is exp in the future relative to the current time?
  5. Does it match the nonce (a one-time random value the client sent with the authorization request, to prevent replay attacks)?

Only once all of these check out do you trust sub and the other payload fields to identify the user.

Don't roll your own UUID + hash implementation

If you build your own ID Token as "a UUID plus a SHA-256 hash," it isn't OpenID Connect compliant, since it can't be signature-verified. Using a JWT library (jose for Node.js, pyjwt for Python, etc.) is the standard approach.

DB Design for Social Login

This is a DB design for storing integration with an external IdP (Identity Provider), like "log in with Google."

Social login (PostgreSQL)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE user_oauth_providers (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(30) NOT NULL, -- 'google' / 'github' / 'apple'
provider_user_id VARCHAR(255) NOT NULL, -- the sub (user ID) within the IdP
access_token TEXT, -- encryption recommended
refresh_token TEXT, -- encryption recommended
token_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ,
PRIMARY KEY (provider, provider_user_id), -- one row per user, per IdP
UNIQUE (user_id, provider) -- one user has at most one link per IdP
);

CREATE INDEX idx_oauth_user ON user_oauth_providers(user_id);

Key points:

  • Making the primary key the composite (provider, provider_user_id) prevents "creating multiple users tied to the same Google account"
  • Encrypt access_token / refresh_token at the application layer before storing them (manage the key separately, e.g. with a KMS)
  • A design where one user can link multiple IdPs (Google + GitHub)

Storing and Refreshing Tokens

There are two kinds of OAuth 2.0 tokens.

TokenPurposeLifetime
Access tokenSent with every API callShort-lived (typically an hour, varies by IdP)
Refresh tokenUsed to get a new access token once it expiresLong-lived (days to months, varies by IdP)
Expiration varies significantly by IdP

Expiration isn't defined by the OAuth 2.0 spec (RFC 6749) — it's up to the IdP's configuration. For example:

  • Google: access token 1 hour / refresh token doesn't expire (as long as it's being used)
  • Facebook: access token 1-2 hours / long-lived access token 60 days
  • Microsoft: access token 1 hour / refresh token 90 days

There's no fixed "industry standard is X hours" value, so check the IdP's documentation.

Refreshing With a Refresh Token

-- Application-layer logic (Node.js pseudocode)
async function getValidAccessToken(userId, provider) {
const row = await db.query(
'SELECT access_token, refresh_token, token_expires_at FROM user_oauth_providers WHERE user_id = $1 AND provider = $2',
[userId, provider]
);

// Refresh if it expires within 5 minutes
if (row.token_expires_at <= new Date(Date.now() + 5 * 60 * 1000)) {
const newTokens = await refreshOauthToken(provider, row.refresh_token);
await db.query(
'UPDATE user_oauth_providers SET access_token = $1, refresh_token = $2, token_expires_at = $3 WHERE user_id = $4 AND provider = $5',
[encrypt(newTokens.access_token), encrypt(newTokens.refresh_token), newTokens.expires_at, userId, provider]
);
return newTokens.access_token;
}

return decrypt(row.access_token);
}

Exercises

Exercise 1: Choose the flow quiz

Choose the best OAuth flow for each of the following scenarios.

  1. Google login on a browser-based SPA (a React app)
  2. Linking with Spotify from a native iOS app
  3. Regularly calling an external service server-to-server (backend ← API)
  4. An old system using Implicit Flow
Sample answer
#FlowReason
1. Google login from an SPAAuthorization Code + PKCEAn SPA is a Public Client; PKCE protects against authorization-code theft
2. A native iOS appAuthorization Code + PKCEA native app is a Public Client too. Using an SDK is the practical approach
3. Server-to-server integrationClient Credentials FlowThis is the server's own access, not on behalf of a user; Authorization Code isn't used
4. Old Implicit FlowMigrate to Authorization Code + PKCEImplicit Flow is deprecated by RFC 9700; plan a migration

Additional note: Client Credentials Flow is for server-to-server integration with no user involved. Example: a backend posting a message via the Slack API.

Common mistakes:

  • "If you have a JWT, you don't need an authorization server": JWT is just a token format; the basic requirement that an authorization server must issue it doesn't change
  • "Implicit Flow is fine for an SPA": outdated information. Even for an SPA, Authorization Code + PKCE is the right answer as of 2026
  • "Client Credentials doesn't need PKCE": Client Credentials Flow has no authorization code involved, so PKCE itself doesn't apply (= there's no authorization-code-theft risk to protect against)
Exercise 2: The steps for JWT verification

You're the Client, and you've received an ID Token issued by Google. Describe the verification steps in 5 steps.

Sample answer
  1. Split the token into three parts: split on . into header.payload.signature
  2. Fetch the public key: fetch jwks_uri from https://accounts.google.com/.well-known/openid-configuration, and pull the public key matching header.kid from that JWKS
  3. Verify the signature: verify the signature over header.payload with the public key, using header.alg (typically RS256)
  4. Verify the claims:
    • Is iss https://accounts.google.com?
    • Is aud your own client ID?
    • Is exp in the future relative to now (exp > now)?
    • Is iat close to the current time (allowing a discrepancy of around abs(iat - now) < 5min)?
    • (If you included a nonce when sending the request) does the nonce match?
  5. If OK, identify the user: treat payload.sub as the "unique ID within Google," match it against user_oauth_providers.provider_user_id, and identify the corresponding user in your own system

An implementation library (Node.js):

import * as jose from 'jose';

const JWKS = jose.createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));

const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer: 'https://accounts.google.com',
audience: process.env.GOOGLE_CLIENT_ID,
});

// payload.sub is the user's ID within Google
const user = await findUserByOauth('google', payload.sub);

Common mistakes:

  • Skipping signature verification: just base64-decoding the payload and using it is critical. An attacker could craft an arbitrary payload
  • Allowing alg: none: a vulnerability in older JWT libraries. Accepting an unsigned JWT means it can be tampered with freely
  • Hardcoding the public key: Google rotates its keys periodically. Fetch it dynamically from the JWKS

Summary

Here's a recap of what this chapter covered.

  • OAuth 2.0 is a protocol for authorization (access rights); OIDC is an authentication (identity verification) extension built on top of OAuth 2.0
  • The modern standard is Authorization Code Flow + PKCE (mandatory for a public client per RFC 9700, a standard feature in OAuth 2.1)
  • Implicit Flow is deprecated and removed (watch out for older tutorials)
  • The ID Token uses the JWT (RFC 7519) format; the required claims are iss / sub / aud / exp / iat
  • JWT verification follows the order "signature → claims → identify the user"; use a JWT library
  • For social login, use (provider, provider_user_id) as the primary key, and store tokens encrypted

That wraps up Part 3 (Applied) of this guide. Starting with the next chapter, we move into the practice section and work through a comprehensive example: requirements → ER diagram → design, end to end.