13.10. OAuth 2.0
Table of Contents
Why OAuth 2.0 Exists
Traditional login systems expect users to create an account and password for every application. This causes several problems:
- Users must trust every app with their password.
- Password reuse becomes common and dangerous.
- External services cannot easily allow third‑party apps to act on a user’s behalf with limited permissions.
OAuth 2.0 is an authorization framework that solves this. It lets one application request limited access to a user’s data or actions in another system, without seeing the user’s password.
Example:
- Your app wants to access a user’s Google Calendar.
- Instead of asking for the user’s Google password, you redirect them to Google.
- Google asks the user to approve access.
- Google sends your app an access token which your app uses to call Google APIs.
Your app never sees the password, only a token with specific, limited permissions.
Key idea: OAuth 2.0 is about authorization, not authentication.
It answers: “Can this app do X on behalf of this user?”
It does not define how to verify identity, even though it is often (incorrectly) used “for login”.
Core Roles in OAuth 2.0
OAuth 2.0 defines four main roles:
| Role | Also called | Description | Example |
|---|---|---|---|
| Resource Owner | User | Entity that owns the data or account | The Google account owner |
| Client | Third‑party app | Application that wants access on behalf of the user | Your calendar integration app |
| Resource Server | API server | Server that hosts the protected resources (APIs) | Google Calendar API |
| Authorization Server | Auth server / IdP | Server that issues tokens after user authorization | accounts.google.com OAuth server |
Sometimes the authorization server and resource server are the same application, sometimes not.
Tokens in OAuth 2.0
OAuth 2.0 works with tokens instead of passwords.
Access Tokens
An access token is a credential that represents authorization to access specific resources.
Properties:
- Short‑lived, for example 5 to 60 minutes.
- Presented with each request, usually in the
Authorizationheader:
GET /user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer ACCESS_TOKEN_HERE- Attached to scopes, which define what actions are allowed.
You usually treat access tokens as opaque strings in your backend unless you use something like JWT, which you can decode.
Refresh Tokens
Access tokens expire quickly to limit damage if they leak. Refresh tokens allow the client to obtain new access tokens without re‑prompting the user.
- Long‑lived, for example hours or days.
- Never sent to resource APIs, only to the authorization server.
- Kept very secure, usually only in a backend or “confidential client”.
Example token flow:
- User signs in and approves the client.
- Authorization server returns:
- access token (short life)
- refresh token (longer life)
- When the access token expires, the client exchanges the refresh token for a fresh access token.
Rule: Never send refresh tokens to the browser or to untrusted client code.
Keep refresh tokens only in secure environments such as server‑side storage.
Scopes
Scopes represent what the client is allowed to do.
Examples:
read:user= read user profilewrite:tasks= create and update tasksemail= read email address
The client requests scopes during authorization:
GET /oauth/authorize?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=read:user write:tasks
&state=xyzThe authorization server shows these scopes to the user:
“This app wants to:
- read your profile
- create and edit your tasks”
The user can approve or deny.
Scopes then become part of the access token, and the resource server uses them to allow or deny API calls.
Examples in your own API:
- Route:
GET /tasksrequirestasks:read. - Route:
POST /tasksrequirestasks:write.
Your backend checks that the access token includes the right scope before performing the operation.
OAuth 2.0 Grant Types
A grant type describes how the client gets an access token.
Modern OAuth 2.0 mostly uses:
- Authorization Code grant (with PKCE for public clients)
- Client Credentials grant (for server to server)
The older Implicit grant is considered insecure and should not be used in new systems.
Authorization Code Grant (with PKCE)
Best for:
- Web apps with a backend server
- Single Page Applications (SPAs)
- Native mobile apps (Android, iOS, desktop) with PKCE
High level flow:
- Client redirects user to authorization server.
- User signs in and approves access.
- Authorization server redirects back with a short‑lived
code. - Client sends this
code(plus client credentials and PKCE data) to the authorization server. - Authorization server returns access token (and optionally refresh token).
This keeps client secrets and tokens off the browser when possible.
Example Flow (Authorization Code)
- Your app redirects the user:
GET https://auth.example.com/oauth/authorize?
response_type=code&
client_id=calendar-app&
redirect_uri=https://myapp.com/oauth/callback&
scope=read:calendar&
state=abc123- User signs in at
auth.example.comand approves. - Authorization server redirects back:
GET /oauth/callback?code=AUTH_CODE_HERE&state=abc123 HTTP/1.1
Host: myapp.com- Your backend exchanges the code:
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=https://myapp.com/oauth/callback&
client_id=calendar-app&
client_secret=YOUR_CLIENT_SECRET- Authorization server returns:
{
"access_token": "ACCESS_TOKEN_HERE",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN_HERE",
"scope": "read:calendar"
}Your backend then stores the tokens securely, for example in a database or session tied to the user.
PKCE (Proof Key for Code Exchange)
PKCE enhances the Authorization Code flow when you cannot keep a client secret private (for example SPA or mobile app).
Idea:
- Client generates a random string
code_verifier. - It hashes it to
code_challengeand sends the challenge when redirecting the user to authorize. - Later, when exchanging the code for tokens, client must present the original
code_verifier. - Authorization server verifies that the verifier matches the initial challenge.
This prevents attackers that steal the authorization code from being able to exchange it for tokens, because they do not have the code_verifier.
Example:
import os, base64, hashlib
# generate code_verifier
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode()
# generate code_challenge
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()You:
- Send
code_challengein the initial/authorizerequest. - Send
code_verifierin the/tokenrequest.
Rule: Use Authorization Code with PKCE for SPAs and native apps.
Use Authorization Code with client secret for traditional server‑side web apps.
Client Credentials Grant
Used when there is no user, only machine to machine communication.
Examples:
- Your backend calls a payment provider’s API.
- A background worker calls another microservice.
Flow:
- Client authenticates with its
client_idandclient_secret. - Authorization server returns an access token that represents the client itself, not a specific user.
Example:
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=service-a&
client_secret=SERVICE_A_SECRET&
scope=orders:readResponse:
{
"access_token": "ACCESS_TOKEN_HERE",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "orders:read"
}You then send this access token in calls from Service A to Service B.
Important: Client Credentials tokens do not represent a user.
Do not use them when you need user identity or per‑user permissions.
Redirect URIs and Security
Redirect URIs are critical in OAuth 2.0 flows that involve a browser.
Rules:
- You must register allowed redirect URIs in the authorization server.
- Authorization server must only redirect to allowed URIs.
- Do not allow wildcards like
https://myapp.com/*if possible. - In development, you can use something like
http://localhost:8000/oauth/callback.
Example configuration:
| Environment | Redirect URI |
|---|---|
| Dev | http://localhost:8000/oauth/callback |
| Staging | https://staging.myapp.com/oauth/callback |
| Prod | https://myapp.com/oauth/callback |
This prevents open redirect vulnerabilities where an attacker tricks the system into sending tokens to a malicious domain.
State Parameter and CSRF Protection
The state parameter protects against CSRF and some redirect attacks.
Flow:
- Before redirecting the user to the authorization server, your app generates a random string and stores it in the session.
- It includes that string as
statein the authorization URL:
GET https://auth.example.com/oauth/authorize?
response_type=code&
client_id=myapp&
redirect_uri=https://myapp.com/oauth/callback&
scope=read:user&
state=RANDOM_STRING- The authorization server returns the same
state:
GET /oauth/callback?code=AUTH_CODE&state=RANDOM_STRING- Your app verifies that the
stateit receives matches the stored value.
If it does not match, it rejects the request.
Rule: Always use state in browser based OAuth flows and always verify it when handling the callback.
Using OAuth 2.0 in Your Own Backend
So far we have talked about OAuth 2.0 as a way to integrate with external providers like Google or GitHub.
You can also use OAuth 2.0 as the foundation of your own authentication and authorization system.
There are two main scenarios:
- Your API as a resource server that validates access tokens.
- Your backend as an OAuth 2.0 client that calls other APIs using OAuth tokens.
Your API as a Resource Server
In this setup, clients send access tokens with API requests and your backend must:
- Validate the token.
- Extract scopes and user identity.
- Apply authorization rules before processing the request.
Example with a Bearer token:
GET /tasks HTTP/1.1
Host: api.example.com
Authorization: Bearer ACCESS_TOKEN_HEREYour FastAPI backend (pseudocode):
from fastapi import Depends, HTTPException, status
from typing import List
class TokenData:
user_id: int
scopes: List[str]
def verify_access_token(token: str) -> TokenData:
# 1. Verify signature / lookup token in DB
# 2. Check expiration
# 3. Extract user_id and scopes
# 4. Raise error if invalid
...
def require_scopes(required_scopes: List[str]):
def dependency(token_data: TokenData = Depends(verify_access_token)):
if not set(required_scopes).issubset(set(token_data.scopes)):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient scope"
)
return token_data
return dependency
@app.get("/tasks")
def list_tasks(token_data: TokenData = Depends(require_scopes(["tasks:read"]))):
# You have token_data.user_id and validated scopes
return get_tasks_for_user(token_data.user_id)Your token validation might:
- Decode a JWT and check its signature, or
- Look up an opaque token in your database or cache.
Your Backend as an OAuth 2.0 Client
Example: Your backend integrates with GitHub on behalf of users.
High level:
- Provide a “Connect GitHub” button that redirects to GitHub’s OAuth authorization URL.
- Handle the callback from GitHub.
- Exchange the code for tokens.
- Store the tokens for that user.
- Use the access token when calling GitHub APIs.
Pseudo FastAPI example:
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse
import httpx
import os
router = APIRouter()
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
GITHUB_REDIRECT_URI = "http://localhost:8000/auth/github/callback"
@router.get("/auth/github")
def github_login():
url = (
"https://github.com/login/oauth/authorize"
f"?client_id={GITHUB_CLIENT_ID}"
f"&redirect_uri={GITHUB_REDIRECT_URI}"
"&scope=read:user"
"&state=random_csrf_token"
)
return RedirectResponse(url)
@router.get("/auth/github/callback")
async def github_callback(code: str, state: str, request: Request):
# TODO check state for CSRF protection
async with httpx.AsyncClient() as client:
token_resp = await client.post(
"https://github.com/login/oauth/access_token",
headers={"Accept": "application/json"},
data={
"client_id": GITHUB_CLIENT_ID,
"client_secret": GITHUB_CLIENT_SECRET,
"code": code,
"redirect_uri": GITHUB_REDIRECT_URI,
},
)
token_data = token_resp.json()
access_token = token_data["access_token"]
# Use token to get user info
async with httpx.AsyncClient() as client:
user_resp = await client.get(
"https://api.github.com/user",
headers={"Authorization": f"Bearer {access_token}"}
)
user_data = user_resp.json()
# Here you would:
# - find or create a local user linked to user_data["id"]
# - store access_token (and refresh_token if provided) securely
# - create a session or JWT for your own app
...This is a typical pattern when you see “Sign in with X” buttons.
Common Pitfalls and Best Practices
Confusing OAuth 2.0 with Authentication
OAuth 2.0 by itself does not tell you how to:
- Get a user’s identity reliably.
- Standardize claims like email, name, etc.
That is what OpenID Connect adds on top of OAuth. It introduces an ID token and standardized user info.
Use:
- OAuth 2.0 for authorization and API access.
- OAuth 2.0 + OpenID Connect when you need login and identity.
Storing Tokens Unsafely
Bad practices:
- Saving access or refresh tokens in plain localStorage or in JavaScript accessible cookies.
- Logging tokens in server logs.
- Sending tokens to third‑party analytics.
Better practices:
- Keep refresh tokens only on backend.
- Use secure, HTTP‑only cookies for session tokens.
- Minimize where tokens are stored and for how long.
- Mask tokens in logs.
Overbroad Scopes
Do not request more scopes than you need. This:
- Increases damage if tokens leak.
- Makes users less likely to approve.
Design small, focused scopes:
profile:read,tasks:read,tasks:write,admin:users.
Not Revoking Tokens
Implement ways to:
- Revoke tokens on user logout if possible.
- Immediately revoke tokens after password change or suspicion of compromise.
- Use short expiration times on access tokens and track refresh tokens to allow revocation.
Summary
- OAuth 2.0 is an authorization framework that lets applications access resources on behalf of a user without handling passwords.
- Main roles: Resource Owner, Client, Authorization Server, Resource Server.
- Main artifacts: access tokens, refresh tokens, scopes.
- Common grants:
- Authorization Code with PKCE for browser and mobile apps.
- Authorization Code with client secret for server‑side web apps.
- Client Credentials for machine to machine communication.
- Use secure redirect URIs,
statefor CSRF protection, and keep secrets and refresh tokens on the server side. - Your backend will often act as:
- A resource server that validates tokens and enforces scopes.
- A client that calls external APIs using OAuth 2.0.
In the next related topics, you will connect OAuth 2.0 with OpenID Connect and JWTs to implement full authentication systems for your backend.
Views: 7
KAHIBARO