The Authorization Code Flow, Step by Step
The flow as HTTP
The previous lesson explained OAuth in plain language. This lesson shows the actual HTTP requests. Every URL, every header, every parameter. This is what your code will do.
Step 1: Redirect the user to the provider
When the user clicks “Log in with GitHub,” your server responds with an HTTP redirect:
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?
client_id=your_client_id&
redirect_uri=http://localhost:3000/auth/github/callback&
scope=read:user user:email&
state=random_string_abc123 The browser follows the redirect to GitHub. Let’s break down the URL parameters:
client_id — Your app’s public identifier. You get this when you register your app with GitHub.
redirect_uri — Where GitHub should send the user after they approve. This must match exactly what you registered with GitHub. If it does not match, GitHub rejects the request. This prevents an attacker from redirecting the user to a malicious site.
scope — What permissions you are requesting. read:user lets you read the user’s profile. user:email lets you read their email addresses. Each provider defines its own scopes.
state — A random string your server generates and remembers. GitHub will send it back unchanged. Your server checks that the returned state matches what it sent. This prevents CSRF attacks. We will cover this in detail in the State Parameter lesson.
Step 2: The user approves on GitHub
GitHub shows the user a consent screen. This happens entirely on GitHub’s site. Your server is not involved.
If the user is not logged in to GitHub, GitHub shows a login form first. If they have two-factor auth enabled, GitHub handles that too. None of this is your problem.
The user clicks “Authorize [your app name].”
Step 3: GitHub redirects back with a code
GitHub redirects the user’s browser to your redirect_uri with two parameters appended:
HTTP/1.1 302 Found
Location: http://localhost:3000/auth/github/callback?
code=abc123def456&
state=random_string_abc123 code — The authorization code. A short-lived, single-use string. Your server exchanges this for an access token in the next step.
state — The same string your server sent in Step 1. Your server must verify this matches before proceeding.
Your server receives this request at GET /auth/github/callback.
Step 4: Exchange the code for an access token
Your server makes a server-to-server POST request to GitHub’s token endpoint. This request does not go through the browser.
POST https://github.com/login/oauth/access_token
Content-Type: application/json
Accept: application/json
{
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"code": "abc123def456",
"redirect_uri": "http://localhost:3000/auth/github/callback"
} client_id — Same as before. Identifies your app.
client_secret — Your app’s private secret. This proves the request is coming from your server, not from someone who intercepted the code. Never expose this value to the browser.
code — The authorization code from Step 3.
redirect_uri — Must match the one from Step 1. GitHub checks this for consistency.
GitHub responds:
{
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"token_type": "bearer",
"scope": "read:user,user:email"
} You now have an access token. This token lets your server call GitHub’s API on behalf of the user.
Step 5: Fetch the user’s profile
Your server uses the access token to call GitHub’s API:
GET https://api.github.com/user
Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a
User-Agent: your-app-name [!NOTE] GitHub’s API requires a
User-Agentheader. Most other providers do not. We will include it in our code.
GitHub responds with the user’s profile:
{
"id": 12345,
"login": "alice",
"name": "Alice Smith",
"email": "[email protected]",
"avatar_url": "https://avatars.githubusercontent.com/u/12345"
} Step 6: Create a local user and session
Your server takes this profile data and:
- Checks if a local user with this GitHub ID already exists
- If yes, uses that user (returning visitor)
- If no, creates a new user with the profile data (first-time login)
- Creates a session (same as password-based auth)
- Sets a session cookie
- Redirects the user to your app (e.g.
/dashboard)
After this step, the user is logged in to your app. The access token has done its job. You can discard it or store it if you need to make more GitHub API calls later.
The full sequence
1. User clicks "Log in with GitHub"
↓
2. Your server → Browser: 302 redirect to GitHub authorization URL
↓
3. Browser → GitHub: User lands on consent screen
↓
4. User clicks "Authorize"
↓
5. GitHub → Browser: 302 redirect to your callback URL with code + state
↓
6. Browser → Your server: GET /auth/github/callback?code=...&state=...
↓
7. Your server → GitHub: POST token endpoint (code + client_secret → access_token)
↓
8. Your server → GitHub: GET /user with access_token → user profile
↓
9. Your server: Create/find user, create session, set cookie
↓
10. Your server → Browser: 302 redirect to /dashboard Steps 7 and 8 are server-to-server. The browser never sees the client secret or the access token.
Exercises
Exercise 1: Draw the sequence on paper or a whiteboard. Label each arrow with who is sending the request (browser, your server, GitHub) and what data is being sent. This is the single most valuable exercise in this course. If you understand the flow, everything else is just code.
Exercise 2: For each step, identify what would happen if it were skipped. What if you did not send the state parameter? What if you did not include the client_secret in the token exchange? What if you skipped the token exchange and tried to use the authorization code as an access token?
Which HTTP request carries the client_secret?
After the OAuth flow completes, how does the user's browser stay logged in to your app?