Skip to main content

OAuth2 Authorization Code Flow: Explained

OAuth2 is a delegation protocol: it lets users grant third-party apps access to their resources without sharing passwords. The authorization code flow is the most secure variant, used by Google, GitHub, and Facebook. It works by having the user log in with the provider, grant permission, and receive an authorization code—which the backend (not the frontend) exchanges for an access token. This separation prevents tokens from being exposed to the browser or intercepted easily.

The OAuth2 Authorization Code Flow

The flow involves four actors: the resource owner (user), the client (your app), the authorization server (e.g., Google), and the resource server (e.g., Google's API). Here's the sequence:

  1. User clicks "Sign in with Google" in your app (client).
  2. Client redirects to Google's authorization endpoint with its client ID and requested scopes.
  3. User logs into Google and grants permission. Google verifies the user and the requested access level.
  4. Google redirects to your callback URL with an authorization code. Code is short-lived (10 min) and single-use.
  5. Client backend exchanges the code for an access token directly with Google (not through the browser).
  6. Google returns an access token (and optionally a refresh token).
  7. Client uses the access token to call Google's API (fetch user profile, email, etc.) on behalf of the user.

The key insight: tokens never touch the browser. The authorization code is useless on its own (only redeemable by your app's registered backend). This prevents XSS attacks from stealing tokens.

When to Use OAuth2 Authorization Code

ScenarioUse OAuth2 Authorization Code?
Let users sign in with Google/GitHub/MicrosoftYes
Access user's Google Drive or GitHub reposYes
Simplify registration (pre-filled user data)Yes
API-to-API authentication (microservices)No — use client credentials flow
Mobile app accessing your own APINo — use JWT tokens issued by your server

Step 1: Register Your Application

To use OAuth2 with Google, GitHub, or another provider:

  1. Go to the provider's developer console (Google Cloud Console, GitHub Settings › Developer Applications).
  2. Create an app, specify your redirect URI (e.g., https://yourapp.com/callback).
  3. Receive a client ID and client secret. Store the secret securely.

The client ID identifies your app; the secret proves ownership to the auth server. Never expose the secret to the frontend.

Step 2: Redirect to Authorization Endpoint

When the user clicks "Sign in," redirect them to the provider's authorization endpoint with required parameters:

# main.py
from fastapi import FastAPI
from urllib.parse import urlencode
import os

app = FastAPI()

GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_REDIRECT_URI = "http://localhost:8000/callback"
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"

@app.get("/login")
async def oauth2_login():
"""Redirect to Google authorization endpoint."""
params = {
"client_id": GOOGLE_CLIENT_ID,
"redirect_uri": GOOGLE_REDIRECT_URI,
"response_type": "code",
"scope": "openid email profile", # Request scopes
"access_type": "offline" # Request refresh token
}
auth_url = f"{GOOGLE_AUTH_URL}?{urlencode(params)}"
return {"auth_url": auth_url}

In a real app, you'd return a 302 redirect or a frontend link. The scopes request specific user data (email, profile picture). access_type=offline requests a refresh token so users stay logged in.

Step 3: Handle the Callback

Google redirects the user back to your callback URI with an authorization code:

# main.py (continued)
from fastapi.responses import RedirectResponse
import httpx
from pydantic import BaseModel

class TokenResponse(BaseModel):
access_token: str
id_token: str | None = None
refresh_token: str | None = None

@app.get("/callback")
async def oauth2_callback(code: str, state: str | None = None):
"""Exchange authorization code for access token."""
if not code:
return {"error": "Missing authorization code"}

# Exchange code for token (backend-to-backend, secret is safe)
async with httpx.AsyncClient() as client:
token_response = await client.post(
GOOGLE_TOKEN_URL,
data={
"client_id": GOOGLE_CLIENT_ID,
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
"code": code,
"redirect_uri": GOOGLE_REDIRECT_URI,
"grant_type": "authorization_code"
}
)

if token_response.status_code != 200:
return {"error": "Failed to exchange code for token"}

tokens = token_response.json()
access_token = tokens["access_token"]

# Optionally fetch user info
user_info_response = await client.get(
"https://openidconnect.googleapis.com/v1/userinfo",
headers={"Authorization": f"Bearer {access_token}"}
)
user_info = user_info_response.json()

# Create session or JWT for your app
# (Example: issue your own JWT with user's Google ID)

return {
"access_token": access_token,
"user": user_info
}

Understanding Scopes

Scopes define what data your app can access. Common OAuth2 scopes:

  • openid: Request an ID token (user identity).
  • email: Access user's email address.
  • profile: Access name, picture, etc.
  • https://www.googleapis.com/auth/drive: Access Google Drive.

The user grants permission for these scopes. Your app can only access what's granted. This principle of least privilege is central to OAuth2.

Step 4: Use the Access Token

With the access token, fetch protected resources:

@app.get("/user-data")
async def get_user_data(access_token: str):
"""Fetch user's Google profile using access token."""
async with httpx.AsyncClient() as client:
response = await client.get(
"https://www.googleapis.com/oauth2/v2/userinfo",
headers={"Authorization": f"Bearer {access_token}"}
)
return response.json()

Refresh Tokens

If access_type=offline was requested, Google issues a refresh token. Access tokens expire (typically 1 hour), but refresh tokens last longer and can issue new access tokens without user intervention:

async def refresh_access_token(refresh_token: str) -> str:
"""Get a new access token using a refresh token."""
async with httpx.AsyncClient() as client:
response = await client.post(
GOOGLE_TOKEN_URL,
data={
"client_id": GOOGLE_CLIENT_ID,
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
"refresh_token": refresh_token,
"grant_type": "refresh_token"
}
)
tokens = response.json()
return tokens["access_token"]

Security Considerations

State Parameter: Prevent CSRF attacks by including a random state value in the authorization request and verifying it in the callback:

import secrets

@app.get("/login")
async def oauth2_login():
state = secrets.token_urlsafe(16)
# Store state in session/cache (e.g., Redis) with TTL
request.session["oauth_state"] = state
params = {"client_id": GOOGLE_CLIENT_ID, ..., "state": state}
...

@app.get("/callback")
async def oauth2_callback(code: str, state: str):
if state != request.session.get("oauth_state"):
return {"error": "CSRF validation failed"}
...

HTTPS Only: OAuth2 credentials must travel over HTTPS. Unencrypted HTTP exposes tokens to network sniffing.

Validate ID Tokens: If using OpenID Connect (built on OAuth2), verify the id_token signature before trusting claims:

from jose import jwt

def verify_id_token(id_token: str, client_id: str) -> dict:
"""Verify Google's ID token signature and claims."""
payload = jwt.get_unverified_claims(id_token)
if payload["aud"] != client_id:
raise ValueError("Token not issued for this app")
if payload["iss"] not in ["https://accounts.google.com", "https://accounts.google.com/"]:
raise ValueError("Token not from Google")
# In production, verify signature using Google's public keys
return payload

Key Takeaways

  • OAuth2 authorization code flow keeps tokens off the browser, making it the safest flow for web apps.
  • The code is exchanged for a token in a backend-to-backend call, protecting the client secret.
  • Scopes limit what data your app can access, implementing least-privilege access.
  • Refresh tokens enable long-lived sessions without repeatedly asking the user to log in.
  • State parameter prevents CSRF attacks in the callback.

Frequently Asked Questions

What's the difference between OAuth2 and OpenID Connect?

OAuth2 is an authorization protocol (delegates access). OpenID Connect (OIDC) adds authentication on top, issuing an ID token that identifies the user. OIDC is a profile of OAuth2; you'll usually see them used together.

Why can't the client exchange the code directly?

The client could leak its client secret to the frontend. Keeping secret exchange in the backend ensures only your trusted server can redeem the code.

What happens if someone steals an authorization code?

An authorization code is single-use and expires in 10 minutes. It's also useless without the client secret. Stolen codes are low-risk compared to leaked access tokens.

Can I use OAuth2 for API-to-API auth?

For service-to-service authentication, use the client credentials flow (no user involved) instead. The authorization code flow is designed for user delegation.

Further Reading