OAuth 2.0 & OpenID Connect — Full Authentication Flow Deep Dive

OAuth 2.0 & OpenID Connect – Full Authentication Flow Deep Dive

OAuth 2.0 & OpenID Connect — Full Authentication Flow Deep Dive

Deep Dive · SP ↔ IdP API Architecture

OAuth 2.0 & OpenID Connect
Full Authentication Flow

Every actor, every HTTP call, every token, and every security mechanism — from the first click to silent token refresh.

24 sequence steps 5 phases 4 actors PKCE · CSRF · Replay protection

Overview

Most developers use OAuth without fully understanding what’s happening under the hood. They plug in a library, get a token back, and move on — until a security bug appears, tokens expire at odd times, or an interview question leaves them blank.

This article walks through every single actor, every API call, every token, and every security mechanism in the OAuth 2.0 + OpenID Connect flow.

Key distinction

OAuth 2.0 is NOT an authentication protocol. It is an authorization framework — it answers “can application X access resource Y on behalf of user Z?” It does not tell you who the user is. OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0 that adds the concept of identity via a cryptographically signed ID token.

The Four Actors

Every arrow in this flow is a communication between two of these four actors:

Actor 1
User / Browser
The human & their user agent
Actor 2
SP / Client App
Your application
Actor 3
Auth Server / IdP
Google, Okta, Azure AD…
Actor 4
Resource Server
The protected API / backend

The Five Phases

Phase 1
Authorization Request + PKCE
Steps 1–4
Phase 2
User Auth & Consent
Steps 5–10
Phase 3
Token Exchange (Back-Channel)
Steps 11–15
Phase 4
Token Validation + UserInfo
Steps 16–19
Phase 5
API Access + Token Refresh
Steps 20–24

Phase 1

Authorization Request + PKCE

The user has landed on your app and clicked “Sign in with Google” (or your IdP of choice). What happens next is a carefully orchestrated sequence of redirects and cryptographic operations — before a single credential has been entered.

1
User clicks Login

The browser sends a request to your SP — your application. Simple.

2
SP generates PKCE parameters

Before doing anything else, your SP generates two values:

  • code_verifier — a cryptographically random string, 43–128 characters, high entropy, completely unpredictable.
  • code_challenge — derived from the verifier by hashing it with SHA-256 and encoding as Base64URL.
code_challenge = BASE64URL(SHA256(code_verifier))

Why PKCE?

PKCE (Proof Key for Code Exchange) solves a critical problem: in the OAuth flow, the SP asks the IdP for an authorization code that travels back through the browser URL. If an attacker intercepts that code, without PKCE they can exchange it for a full access token. With PKCE, they’re stuck — to exchange the code they need the original code_verifier, which was never sent over the network. SHA-256 is a one-way function; the verifier cannot be derived from the challenge.

3
SP builds the Authorization URL and sends a 302 redirect

The redirect points to the IdP’s /authorize endpoint with these query parameters:

response_type=code
client_id=YOUR_APP_ID
redirect_uri=https://yourapp.com/callback
scope=openid profile email
state=RANDOM_CSRF_VALUE
nonce=RANDOM_REPLAY_VALUE
code_challenge=BASE64URL_HASH
code_challenge_method=S256

The openid scope is what turns this into an OIDC request. state is CSRF protection. nonce prevents ID token replay attacks.

4
Browser follows the redirect to the IdP

The browser makes a GET request to the IdP’s /authorize endpoint. The user is now on the IdP’s domain — not yours. Your app never sees the user’s password.


Phase 2

User Authentication & Consent

Everything in this phase happens on the IdP’s domain. Your SP has no visibility into it — intentionally.

5
IdP presents the login UI

The IdP renders an HTML login form on its own domain. Your SP cannot intercept the password. The trust is entirely in the IdP.

6
User submits credentials

A POST request goes to the IdP with the username and password.

7
IdP validates credentials and runs policies

This internal step can include: verifying the password hash, triggering MFA (TOTP, push notification, hardware key), checking device trust policies, evaluating risk signals (unusual location, new device), and verifying account status. All of this must pass before proceeding.

8
Consent screen

The IdP shows the user exactly what scopes the application is requesting: “Application X wants access to your name, email, and profile picture.” For enterprise apps with admin pre-authorization, this step is skipped. For consumer OAuth, the user must explicitly approve.

9
User grants consent

The user clicks Allow.

10
IdP issues authorization code and redirects back

The IdP generates a short-lived, single-use authorization code — valid for ~10 minutes, usable exactly once. Think of it as a claim ticket at a coat check. It’s not the coat; it’s the ticket to get the coat. The IdP sends a 302 redirect to your SP’s redirect_uri:

https://yourapp.com/callback?code=AUTH_CODE&state=xyz

The code traveled through the browser (visible in the URL). That’s fine — it’s worthless without the code_verifier, and it expires in 10 minutes.


Phase 3

Token Exchange (Back-Channel)

This is arguably the most important phase from a security standpoint. Welcome to the back-channel — server-to-server communication that never touches the browser.

11
Browser delivers the auth code to your SP

The browser follows the redirect and hits your SP’s callback URL with code and state in the query string. Your SP now has the authorization code.

12
SP validates the state parameter

Before doing ANYTHING with the code, your SP must verify that the state value matches what it generated in Step 3. This is CSRF protection. If an attacker tried to trick your app into making a forged OAuth request, the state wouldn’t match.

Critical

If state doesn’t match — abort immediately. Return an error. Do not proceed under any circumstances.

13
SP POSTs to the /token endpoint (server-to-server)

This does NOT go through the browser. Your SP makes a direct HTTPS POST to the IdP:

POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&client_id=YOUR_APP_ID
&client_secret=YOUR_APP_SECRET
&redirect_uri=https://yourapp.com/callback
&code_verifier=ORIGINAL_RANDOM_STRING

The code_verifier is being sent for the first time here — it has never appeared in a browser URL. It went from your SP’s memory directly to the IdP’s server over HTTPS.

14
IdP performs the PKCE check

The IdP takes the code_verifier you sent, runs it through SHA256(), Base64URL-encodes it, and compares it against the stored code_challenge from Step 3:

SHA256(code_verifier) == stored_code_challenge ?

If yes — proceed. If no — reject (stolen code attempt). The IdP also verifies the code hasn’t expired, hasn’t already been used, and that the client_id and redirect_uri match.

15
IdP returns the Token Response

If all checks pass, the IdP returns a JSON object with three tokens:

{
  "access_token":  "eyJ...",   // Short-lived JWT (5–60 min)
  "id_token":      "eyJ...",   // OIDC identity JWT
  "refresh_token": "dGhp...",  // Long-lived, for silent renewal
  "token_type":    "Bearer",
  "expires_in":    3600
}

These tokens never touched the browser. They went directly from IdP to SP, server-to-server.


Phase 4

Token Validation & UserInfo

Having a token isn’t enough. You must validate it. An unvalidated token is worse than no token — it’s a false sense of security.

16
SP validates the ID token

The id_token is a JWT with three Base64URL-encoded parts separated by dots: header, payload, signature. Your SP must verify, in order:

  • Signature — verified with the IdP’s public key. Tampered? Reject.
  • iss (issuer) — must match the IdP’s URL.
  • aud (audience) — must include your SP’s client_id.
  • exp (expiry) — current time must be before exp.
  • iat (issued at) — must be in the past, not the future.
  • nonce — must match the nonce sent in Step 3. Prevents replay attacks.
  • at_hash — hash of the access_token, if present.

Never do this

Never accept alg: "none". There is a well-known attack where an attacker strips the signature and sets the algorithm to “none,” hoping the library accepts it. Always enforce RS256 or ES256 explicitly in your JWT validation configuration.

17
Fetch JWKS (if needed)

To verify the signature, your SP fetches the IdP’s public keys from its JWKS (JSON Web Key Set) endpoint — typically at /.well-known/jwks.json. The kid field in the JWT header identifies which key to use. These keys are cached; you don’t refetch on every login, only when keys rotate.

18
Call the /userinfo endpoint

Your SP may call the IdP’s /userinfo endpoint with the access_token as a Bearer token to receive richer profile claims: full name, email, profile picture, locale, and custom attributes. The ID token has a fixed size limit; /userinfo is the channel for extended data.

GET /userinfo HTTP/1.1
Authorization: Bearer {access_token}
19
SP establishes the application session

Your SP now has everything it needs. It creates a server-side session or issues its own application-level tokens (e.g., a session cookie). The user is redirected to their destination. The entire OAuth/OIDC dance is invisible to them — they simply “logged in.”


Phase 5

API Access & Token Refresh

Steady state — the ongoing lifecycle after login. Every user action in your app triggers this phase.

20
User makes an authenticated request

The user clicks something — views their profile, loads a dashboard. The browser sends a request to your SP.

21
SP calls the Resource Server with the access token

Your SP attaches the access_token as a Bearer token and calls the protected API:

GET /api/resource HTTP/1.1
Authorization: Bearer {access_token}
22
Resource Server validates the access token — locally

Because JWTs are self-contained, the Resource Server can validate the access_token without calling the IdP. It verifies the JWT signature with the cached public key, checks exp, confirms aud matches its own identifier, and checks scope claims match what this endpoint requires. No network call to the IdP — this is why JWTs are so powerful in microservice architectures.

23
Resource Server returns protected data

All checks pass — 200 OK, here’s your data.

24
SP silently refreshes using the refresh token

When the SP gets a 401 or detects the access token is near expiry, it POSTs to the IdP’s /token endpoint silently:

grant_type=refresh_token
&refresh_token={stored_refresh_token}
&client_id=YOUR_APP_ID
&client_secret=YOUR_APP_SECRET

The IdP returns a new access_token and a NEW refresh_token. The old refresh token is immediately invalidated.

Refresh Token Rotation

Each refresh token use issues a replacement. If an attacker steals a refresh token and tries to use it after you already have — the IdP detects that a spent token is being replayed. This is a theft signal. The IdP can revoke the entire token family, log the user out, and alert the security team. The refresh token is a shared secret; the moment a second party uses it, the alarm goes off.

What triggers full re-authentication?

Refresh tokens can expire (typically 24 hours to 90 days) or be revoked by: the user logging out, an admin revoking the session, or a security policy trigger (password changed, suspicious location). When the refresh token is gone, the user goes through the full Phase 1–4 flow again.


Summary

Phase 1

SP generates PKCE params and redirects to /authorize. No credentials yet — just cryptographic groundwork.

Phase 2

User authenticates at the IdP. MFA runs here. User consents to scopes. IdP issues a short-lived authorization code.

Phase 3

Back-channel token exchange. SP sends code + code_verifier to /token. IdP verifies PKCE and issues all three tokens.

Phase 4

SP validates ID token — signature, issuer, audience, expiry, nonce. Calls /userinfo. Establishes the session.

Phase 5

Steady state. Every API call carries a Bearer access token. Resource Server validates locally. When expired, SP refreshes silently — with rotation providing automatic theft detection.

Security properties baked into every step

  • PKCE — stolen authorization codes cannot be exchanged for tokens
  • state — prevents CSRF attacks on the OAuth flow
  • nonce — prevents ID token replay attacks across sessions
  • Back-channel exchange — final tokens never touch the browser
  • JWT local validation — no central auth bottleneck at the API layer
  • Refresh token rotation — automatic detection of session theft

The bigger picture

This is not just a login system. It is a carefully engineered trust delegation protocol. Every parameter has a reason. Every redirect serves a purpose. Every hash and signature protects against a specific real-world attack.


Quick Reference Cheat Sheet

TermWhat it is
SPService Provider — your application
IdPIdentity Provider — Google, Okta, Azure AD, etc.
OAuth 2.0Authorization framework — what can you access?
OIDCOpenID Connect — who are you? (built on OAuth 2.0)
code_verifierRandom secret generated by SP — never sent through browser
code_challengeSHA256(code_verifier) — sent to IdP in /authorize
authorization codeOne-time, short-lived ticket from IdP to SP (~10 min)
access_tokenJWT used to call protected APIs (5–60 min)
id_tokenJWT containing user identity claims (OIDC-specific)
refresh_tokenLong-lived credential to get new access tokens silently
PKCEProof Key for Code Exchange — prevents code interception
stateCSRF protection parameter generated by the SP
nonceReplay attack prevention for ID tokens
JWKSJSON Web Key Set — IdP’s public keys for signature verification
back-channelServer-to-server communication (not through browser)
front-channelCommunication via browser redirects (visible in URL)
Bearer tokenAuthorization: Bearer {token} HTTP header pattern
RT rotationEach refresh token use issues a new one; old is immediately invalidated
OAuth 2.0 / OpenID Connect — SP ↔ IdP API Architecture Deep Dive · 24 steps · 5 phases

Comments

Leave a Reply

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