Build an Auth Code Flow Client
Build an authorization code flow client in this hands-on OAuth 2 · OpenID Connect tutorial. Step-by-step guidance, troubleshooting, and next steps for developers learning by doing.
Focus: build an authorization code flow client
You know the theory: OAuth 2 can get you an access token, and OpenID Connect layers identity on top. But when you sit down to actually build an authorization code flow client from scratch, the screen goes blank. Where do you start? What endpoints do you need? How do you handle the redirect, exchange the code, and store the token without falling into security traps? This lesson takes you from theory to working code—no magic libraries, no black boxes. You'll build a minimal but complete auth code flow client in Python, step by step, and walk away ready to plug it into real apps.
The problem this lesson solves
Most OAuth tutorials either stop at the diagram or push you straight into a heavyweight library that hides everything. You're left with two equally bad outcomes: you don't really understand the flow, or you can't debug when things go wrong. That's the pain this lesson kills.
The authorization code flow is the backbone of secure OAuth — it's what Google, GitHub, Auth0, and every major provider use for web and mobile apps. Without a solid grasp of how to build a client for it, you'll either paste together code you don't understand or spend hours wresting with a framework that hides the details. This lesson gives you a rock-solid mental model, then a hands-on implementation you can run, poke at, and extend.
Core concept / mental model
Think of the authorization code flow as a three-party dance with a golden ticket. You have:
- The client (your app) — asks for the ticket
- The authorization server (e.g., Auth0, Okta, your own) — issues the ticket
- The user (resource owner) — grants permission
The dance has two acts, each using a separate channel:
- Act 1 (front channel): The client redirects the user to the authorization server. The user logs in, and the server sends back a short-lived authorization code via the browser redirect. This channel is visible to the user's browser, so it must never carry secrets.
- Act 2 (back channel): The client sends that code to the server's token endpoint, along with its
client_idandclient_secret, and receives an access token (and optionally an ID token). This channel is a direct server-to-server request, safe from browser snooping.
The code is like a golden ticket — it's single-use, short-lived (often 60 seconds), and worthless unless the server can spot the client's secret. That's why the code flow is considered the most secure: the access token never appears in the browser URL.
Pro tip: The authorization code flow is the recommended flow for
web appsandnative/mobile apps. The implicit flow, where tokens are returned in the redirect, is a thing of the past—avoid it unless you have no other choice.
How it works step by step
Let's break down the exact steps, because the order matters as much as the actions.
The full sequence
- Client prepares an authorization request — It builds a URL pointing to the authorization endpoint, with parameters like
response_type=code,client_id,redirect_uri,scope, and a randomstate. - Redirect to authorization server — The client sends the user to that URL. The server handles login and consent.
- User consents — The server asks the user to approve the requested scopes (or silently grants if already logged in).
- Authorization code returned — The server redirects the user back to the client's
redirect_uri, appending?code=...&state=.... - Client validates
state— This is critical to prevent CSRF. Ifstatedoesn't match what you started with, abort. - Token exchange — The client sends a POST to the token endpoint with the code,
client_id,client_secret,redirect_uri, andgrant_type=authorization_code. - Server returns tokens — You get an
access_token, possibly anid_token, and arefresh_token. - Client stores tokens safely — Typically in memory or a secure session, never in JavaScript or logs.
Why state matters
stateis your shield against CSRF. An attacker could otherwise initiate the flow, then trick your user into completing it with a code your app didn't ask for. Always generate a random value, store it, and verify it on the callback.
Hands-on walkthrough
Now for the fun part. We'll build a minimal Python client using only the standard library and requests. No magic. Let's do it step by step.
Prerequisites
- Python 3.10+ installed.
requestslibrary (pip install requests).- A test OAuth provider. You can register a free app with Auth0, Okta, or use a local mock server.
For this walkthrough, we'll assume:
AUTH_URL = 'https://your-provider.com/authorize'TOKEN_URL = 'https://your-provider.com/oauth/token'CLIENT_ID = 'your-client-id'CLIENT_SECRET = 'your-client-secret'REDIRECT_URI = 'http://localhost:8000/callback'- Scopes:
openid profile email(to get identity too)
Step 1: Build the authorization URL
import secrets
from urllib.parse import urlencode
# Generate a random state value for CSRF protection
state = secrets.token_urlsafe(32)
# In a real app, store this in the user's session
session['oauth_state'] = state
params = {
'response_type': 'code',
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI,
'scope': 'openid profile email',
'state': state,
}
auth_url = f'{AUTH_URL}?{urlencode(params)}'
print('Redirect the user to:', auth_url)
Output: A long URL that sends the user to the provider's login page.
Step 2: Handle the callback
In your web framework (e.g., Flask), handle the redirect to /callback. Here's a minimal Flask example:
from flask import Flask, request, session, redirect
import requests
app = Flask(__name__)
app.secret_key = 'super-secret-key' # in practice, from environment
@app.route('/callback')
def callback():
# Check the state parameter to prevent CSRF
if request.args.get('state') != session.get('oauth_state'):
return 'State mismatch! Possible CSRF attack.', 400
code = request.args.get('code')
if not code:
return 'No code returned.', 400
# Exchange the code for tokens
response = requests.post(TOKEN_URL, data={
'grant_type': 'authorization_code',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': code,
'redirect_uri': REDIRECT_URI,
})
if response.status_code != 200:
return f'Token exchange failed: {response.text}', response.status_code
tokens = response.json()
access_token = tokens['access_token']
id_token = tokens.get('id_token')
refresh_token = tokens.get('refresh_token')
# Store access token securely (in memory or a secure session)
session['access_token'] = access_token
# Optionally decode ID token to get user info (OIDC)
# This is just a placeholder for now — you'd validate the JWT properly
return f'Logged in! Token: {access_token[:10]}...'
if __name__ == '__main__':
app.run(port=8000)
Expected behavior: When a user visits the callback with a valid code, the server exchanges it for a token and prints a success message.
Step 3: Use the access token to call an API
Once you have the token, you can call the provider's resource server:
response = requests.get('https://api.example.com/profile',
headers={'Authorization': f'Bearer {access_token}'})
print(response.json())
Pro tip: Never put
client_secretin client-side code. The code flow keeps it on the server, which is why it's safe for web apps.
Compare options / when to choose what
Now that you've built the code flow client, let's see how it stacks up against other flows. Here's a comparison table:
| Flow | Use case | Security | Complexity | Browser involvement |
|---|---|---|---|---|
| Authorization Code | Traditional web apps, mobile/native | High (secret kept server-side) | Moderate | High (redirects) |
| Authorization Code + PKCE | SPAs, mobile apps, public clients | High (no secret, or secretless) | Moderate | High |
| Implicit (deprecated) | Old SPAs | Low (tokens in browser) | Low | High |
| Client Credentials | Server-to-server, machine-to-machine | High | Low | None |
| Resource Owner Password | Legacy, highly trusted first-party | Low (user gives password) | Low | None |
When to choose which
- Choose the standard Authorization Code for any web app where you can hold a client secret on a server. PKCE is required for public clients—SPAs or mobile apps that can't keep a secret.
- Prefer Client Credentials when you have no user session, e.g., cron jobs, services.
- Avoid Implicit—it's deprecated by the OAuth 2.1 spec. Resource Owner Password is only for legacy/trusted scenarios; steer clear.
Troubleshooting & edge cases
Here are the most common pitfalls when building an authorization code flow client, and how to fix them.
redirect_uri mismatch
If the provider returns invalid_request or redirect_uri_mismatch, double-check that the URI in your code exactly matches one registered with the provider, including scheme, host, port, and path. Trailing slashes matter!
state isn't matching
You'll see a “State mismatch” error or the callback fails. Make sure you generate a new state for each authorization request and store it server-side before redirection. Don't reuse a stale value.
invalid_grant during token exchange
This often means the code was already used, expired, or the redirect_uri you sent during exchange doesn't match the one used in the authorization request. Codes are single-use—always request a new one for each login.
Missing id_token
If you requested openid scope but don't get an ID token, check your provider's configuration—some only issue it under certain conditions. Ensure you're using the correct scope.
access_denied
This happens when the user refuses the consent screen. Handle this gracefully in your UI.
CORS errors when calling APIs from browser
If you're building an SPA and fetching APIs directly with the access token, ensure your resource server sends the right CORS headers. Better yet, proxy calls through your backend.
Pro tip: Always log the full provider response when debugging. The error
descriptionfield often tells you exactly what's wrong.
What you learned & what's next
You've now built a working authorization code flow client from scratch. You can explain the three-party dance, implement each step, and handle common errors. You know how to validate state, exchange codes, and store tokens safely. You've also compared the code flow to alternatives and know when to use each.
You're ready to secure your client even further — next up, we'll cover PKCE (Proof Key for Code Exchange) to make your flow bulletproof for public clients like SPAs and mobile apps. You'll learn to generate a code verifier and challenge, and upgrade your client in minutes. Stay tuned!
Practice recap
Now it's your turn: take the Flask example and add PKCE support. Generate a code_verifier and code_challenge, include them in the authorization URL, and send code_verifier during the token exchange. Test it against a public client setup in your provider to see the difference. This hands-on exercise cements the flow and gets you ready for the next lesson!
Common mistakes
- Forgetting to validate the
stateparameter on the callback, leaving your app open to CSRF attacks. - Hardcoding the
client_secretin JavaScript or storing it in local storage for a public client. - Reusing an authorization code across requests — codes are single-use and expire quickly.
- Setting
redirect_uriinconsistently between the authorization request and the token exchange, causingredirect_uri_mismatcherrors.
Variations
- Use PKCE (
code_challengeandcode_verifier) when building public clients like SPAs or mobile apps. - Instead of a raw
requestscall, use an OAuth library likerequests-oauthliborAuthlibto abstract token storage and refresh. - Implement the flow with a managed service like Auth0, Okta, or AWS Cognito, which simplifies registration and token management.
Real-world use cases
- Authenticating users in a Django or Flask web app to access a protected API from a service like GitHub or Google.
- Building a mobile app that logs in users with a provider like Auth0 and needs secure token handling without exposing secrets.
- Creating a CLI tool that asks users to authorize via a browser, receives a code, and exchanges it for a token to access cloud resources.
Key takeaways
- The authorization code flow involves two channels: the front channel for redirects and the back channel for token exchange — never put secrets in the browser.
stateis your CSRF shield—always generate and verify it.- The
redirect_urimust match registration exactly across both calls. - Codes are single-use and short-lived; never attempt to reuse them.
- Store access tokens securely on the server, not in client-side storage.
- For public clients, use PKCE to eliminate the need for a client secret.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.