Table of Contents
1011 words
5 minutes
OAuth 2.0 Authorization Flows: A Complete Visual Guide

Introduction: What Problem Does OAuth 2.0 Actually Solve#

Your website wants to help users “log in with GitHub,” but the prerequisite is obtaining read access to the user’s GitHub account. But why should users hand over their GitHub password to you? OAuth 2.0’s answer is: don’t hand over the password, hand over a token.

Its core idea can be compressed into one sentence: The authorization server (GitHub) issues a token (Access Token) to a third-party application (your website) that has limited permissions, a limited lifetime, and can be revoked independently, rather than handing over the account password. Once you understand this, the differences between the four authorization flows are simply “how this token is obtained, and in what scenario.”

Four Roles: First, Distinguish Who Is Who#

Any OAuth flow revolves around four roles:

RoleExampleOne-Sentence Responsibility
Resource OwnerUserThe person who owns the data and can approve authorization
ClientYour websiteThe third-party application that wants to access user data
Authorization ServerGitHub’s login pageVerifies user identity, issues tokens
Resource ServerGitHub APIValidates tokens, provides data

The most common misconception is treating the “authorization server” and “resource server” as the same thing—in many scenarios they are indeed the same service (GitHub is both), but conceptually they must be separated: one issues tokens, the other validates tokens and serves data.

Authorization Code Flow: The Standard Answer for Web Applications#

This is the most commonly used and most secure flow, because it guarantees that the Access Token only passes through the backend and never enters the browser.

Sequence#

User ──① Clicks "Sign in with GitHub" ──▶ Client (your backend)
User ◀──② 302 redirect to GitHub authorization page── Client
User ──③ Enters username and password, clicks authorize──▶ GitHub authorization server
User ◀──④ 302 redirect back to callback address, URL carries ?code=xxxx── GitHub
Client ──⑤ Backend exchanges code + client_secret for token──▶ GitHub
Client ◀──⑥ Returns access_token + refresh_token── GitHub
Client ──⑦ Requests API with access_token──▶ GitHub API

① The frontend directs the user to the authorization endpoint:

https://github.com/login/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=user:email
&state=random-string

The state parameter is key to preventing CSRF: upon callback, it must be verified that it matches the one from initiation, otherwise an attacker could trick the user into completing an authorization you are unaware of.

② After the authorization server verifies the user’s identity, it 302-redirects back to your callback URL, carrying a one-time code.

③ The key step: Only your backend, after receiving the code, uses the client_id + client_secret to exchange for a token. Because the client_secret only exists on the server, no script in the browser can forge this step:

Terminal window
curl -X POST https://github.com/login/oauth/access_token \
-H "Accept: application/json" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code=THE_ONE_TIME_CODE" \
-d "redirect_uri=https://your-app.com/callback"
//Backend token exchange logic written in Node (illustrative)
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code, //One-time authorization code from the callback URL
redirect_uri: 'https://your-app.com/callback',
}),
});
const { access_token } = await tokenRes.json();

Why the code Is One-Time Use#

The code typically has a validity period of only tens of seconds to a few minutes, and becomes invalid after a single use. Even if it is leaked during transmission in the callback URL, an attacker cannot obtain the client_secret and cannot redeem it for a token—this “two-legged” design is the foundation of the authorization code flow’s security.

PKCE: Authorization Code Without a client_secret (Native Apps / SPA)#

In mobile apps or pure frontend SPAs, there is no way to securely store the client_secret. PKCE (RFC 7636)‘s solution is: the client generates its own random key (code_verifier), sends its hash (code_challenge) to the authorization server during authorization, and hands over the original value when exchanging for a token—only the client that initiated the authorization knows this value, and it’s useless even if a man-in-the-middle intercepts the code.

//Generate code_verifier and code_challenge
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
//Include the challenge during authorization
// ?client_id=...&code_challenge=${challenge}&code_challenge_method=S256
//Include the verifier during token exchange; the authorization server issues the token only if the hashes match

Today, GitHub, Google, and Auth0 all require or strongly recommend PKCE. Even if your application is a traditional web backend, adding PKCE only has benefits.

The Other Three Flows: Know Them in Order to Avoid Them#

  • Implicit Grant: An early flow designed for pure frontends, where the Access Token appears directly in the browser’s address bar, with an extremely large exposure surface. RFC 6749 was explicitly marked as not recommended for use by RFC 8252/9700 in 2019. For SPAs, use “Authorization Code + PKCE” instead.
  • Resource Owner Password Credentials (ROPC): The client directly collects the username and password to exchange for a token. This is equivalent to handing the password to a third-party application, and it has been removed in the OAuth 2.1 draft. It is only suitable for high-trust migration scenarios within the same company.
  • Client Credentials: No user involvement; the client uses the client_secret to directly exchange for a token belonging to itself, used for service-to-service (M2M) communication, such as your backend calling an internal API:
Terminal window
curl -X POST https://auth.example.com/token \
-d "grant_type=client_credentials" \
-d "client_id=SERVICE_A" \
-d "client_secret=SECRET" \
-d "scope=internal:read"

Selection Quick Reference Table#

Application TypeRecommended FlowReason
Traditional Web Application (with backend)Authorization Code (optionally with PKCE)Token doesn’t enter the browser
SPA / Native AppAuthorization Code + PKCECannot securely store a secret
Service-to-service callsClient CredentialsNo user, machine identity
First-party high-trust applicationsROPC (avoid if possible)Compatibility with legacy systems
Pure frontend legacy applicationsImplicitDeprecated, do not use

Conclusion#

The four OAuth 2.0 flows may look complex, but in essence they are just different answers to three questions: Who is authorizing (a user or a machine)? Can the client securely store a key? How far can the token reach? As a beginner, remembering just one rule—“prefer Authorization Code + PKCE, use Client Credentials for M2M”—already helps you avoid 90% of the pitfalls. As for the remaining 10%, when you actually encounter refresh token rotation, token revocation, and scope minimization, it won’t be too late to come back and consult the spec.


References#

OAuth 2.0 Authorization Flows: A Complete Visual Guide
https://www.hehonglei.com/posts/oauth2-authorization-flows-guide/
Author
HHLBlog
Published at
2026-09-09
License
CC BY-NC-SA 4.0