OAuth 2.0 and OpenID Connect (OIDC) are the backbone of modern authentication and authorization. Whether you’re building a web app, mobile app, or API, understanding these protocols is essential.
In this guide, we’ll explore OAuth 2.0 flows, OIDC, tokens, and security best practices.
Understanding OAuth 2.0
The Authorization Problem
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Before OAuth: The Problem โ
โ โ
โ User โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโ Password โโโโโโโโโโโโ โ
โ โ My App โ โโโโโโโโโโโโบ โ Photo โ โ
โ โ โ โ Site โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ โ
โ โ "Give me your password so I can โ
โ โ access your photos" โ
โ โผ โ
โ โ App sees all user credentials! โ
โ โ App gets full access to account โ
โ โ User can't revoke app access โ
โ โ Compromise = full account compromise โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ After OAuth: The Solution โ
โ โ
โ User โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ My App โ โโ Token โโโโบ โ Photo โ โ
โ โ โ โ Site โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโ "Authorize" โโโโโโโโโโโโ โ
โ โ Auth โ โโโโโโโโโโโโโโโ โ User โ โ
โ โ Server โ โ Browser โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โ โ
App never sees password โ
โ โ
Limited access (scopes) โ
โ โ
User can revoke access โ
โ โ
Tokens can expire โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
OAuth 2.0 Roles
oauth_roles = {
"resource_owner": {
"description": "The user",
"example": "You, granting access to your photos"
},
"client": {
"description": "The application requesting access",
"example": "My Photo Printing App"
},
"authorization_server": {
"description": "The server that authenticates and issues tokens",
"example": "Google, Auth0, Okta"
},
"resource_server": {
"description": "The API that hosts protected resources",
"example": "Google Photos API"
}
}
OAuth 2.0 Grant Types
# 1. Authorization Code Flow (for web apps)
# Most secure, uses server-side token exchange
flow_authorization_code:
steps:
- "User clicks 'Login with Google'"
- "Redirect to Google OAuth"
- "User authorizes"
- "Redirect back with auth code"
- "Server exchanges code for token"
use_when:
- "Server-side web apps"
- "Mobile apps (with PKCE)"
# 2. Implicit Flow (deprecated)
# Don't use - security issues
# 3. Client Credentials Flow
# Server-to-server communication
flow_client_credentials:
use_when:
- "Machine-to-machine"
- "Background jobs"
example:
"Service A accessing Service B"
# 4. Device Code Flow
# For devices without browsers
# 5. Refresh Token Flow
# Get new access tokens
Authorization Code Flow
Step-by-Step
# Authorization Code Flow
class AuthorizationCodeFlow:
"""Web app OAuth flow"""
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_authorization_url(self, state, scope):
"""Step 1: Get URL to redirect user"""
params = {
"response_type": "code",
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"scope": scope,
"state": state # CSRF protection
}
# Example: https://auth.example.com/authorize?...
return f"https://auth.example.com/authorize?" \
f"{urllib.parse.urlencode(params)}"
def exchange_code_for_token(self, code):
"""Step 2: Exchange code for token"""
response = requests.post(
"https://auth.example.com/token",
data={
"grant_type": "authorization_code",
"code": code,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": self.redirect_uri
}
)
return response.json()
def refresh_token(self, refresh_token):
"""Step 3: Get new access token"""
response = requests.post(
"https://auth.example.com/token",
data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": self.client_id,
"client_secret": self.client_secret
}
)
return response.json()
Implementation Example
# Flask implementation
from flask import Flask, request, session, redirect
import requests
app = Flask(__name__)
app.secret_key = "your-secret-key"
# Configuration
AUTH_SERVER = "https://accounts.google.com"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
REDIRECT_URI = "https://yourapp.com/callback"
@app.route("/login")
def login():
state = secrets.token_hex(16)
session["oauth_state"] = state
auth_url = (
f"{AUTH_SERVER}/o/oauth2/v2/auth?"
f"client_id={CLIENT_ID}&"
f"redirect_uri={REDIRECT_URI}&"
f"response_type=code&"
f"scope=openid%20email%20profile&"
f"state={state}"
)
return redirect(auth_url)
@app.route("/callback")
def callback():
# Verify state
if request.args.get("state") != session.get("oauth_state"):
return "Invalid state", 400
# Exchange code for token
token_response = requests.post(
f"{AUTH_SERVER}/token",
data={
"code": request.args.get("code"),
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code"
}
).json()
# Get user info
user_info = requests.get(
f"{AUTH_SERVER}/userinfo",
headers={"Authorization": f"Bearer {token_response['access_token']}"}
).json()
# Create session
session["user"] = user_info
session["access_token"] = token_response["access_token"]
return redirect("/dashboard")
OpenID Connect (OIDC)
OIDC Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OpenID Connect Layer โ
โ โ
โ OAuth 2.0: Authorization โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ + UserInfo endpoint โ โ
โ โ + ID Token (JWT) โ โ
โ โ + Standard scopes (openid, profile, email) โ โ
โ โ + Discovery protocol โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ = Authentication + Authorization โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ID Token vs Access Token
token_comparison = {
"id_token": {
"purpose": "Authentication (who is the user)",
"audience": "Client application",
"format": "JWT",
"contains": "User claims (sub, name, email)",
"verified_by": "Signature + claims"
},
"access_token": {
"purpose": "Authorization (what can they access)",
"audience": "Resource server (API)",
"format": "Opaque or JWT",
"contains": "Scopes, user, client info",
"verified_by": "Token introspection"
}
}
ID Token Claims
# Example ID Token (decoded)
id_token = {
"iss": "https://accounts.google.com", # Issuer
"azp": "client-id.apps.googleusercontent.com", # Authorized party
"aud": "client-id.apps.googleusercontent.com", # Audience
"sub": "1234567890", # Subject (user ID)
"at_hash": "access_token_hash", # Access token hash
"iat": 1516239022, # Issued at
"exp": 1516242622, # Expiration
# Custom claims
"name": "John Doe",
"picture": "https://...",
"email": "[email protected]",
"email_verified": True,
"locale": "en"
}
OIDC Discovery
# OIDC Discovery endpoint
# GET https://accounts.google.com/.well-known/openid-configuration
discovery_document = {
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"response_types_supported": ["code", "token", "id_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"]
}
# Use discovery to configure client
import requests
def get_oidc_config(issuer):
well_known = f"{issuer}/.well-known/openid-configuration"
return requests.get(well_known).json()
config = get_oidc_config("https://accounts.google.com")
Token Security
Token Types
# Access Token - Short-lived
access_token = {
"lifetime": "15 minutes to 1 hour",
"purpose": "Access protected resources",
"storage": "Server memory (not localStorage!)"
}
# Refresh Token - Long-lived
refresh_token = {
"lifetime": "Days to weeks",
"purpose": "Get new access tokens",
"storage": "Secure server-side storage, encrypted"
}
# ID Token - For authentication
id_token = {
"lifetime": "15 minutes to 1 hour",
"purpose": "Verify user identity",
"storage": "Session storage"
}
Token Storage
# SECURE storage
# Frontend (SPA)
secure_storage = {
"access_token": {
"where": "JavaScript memory only",
"why": "XSS can't steal from memory"
},
"refresh_token": {
"where": "HTTP-only cookie (server-side)",
"why": "JavaScript can't read HTTP-only cookies"
}
}
# Backend - Always encrypt refresh tokens
import cryptography
class TokenStore:
def __init__(self, key):
self.cipher = cryptography.fernet.Fernet(key)
def store(self, user_id, refresh_token):
encrypted = self.cipher.encrypt(refresh_token.encode())
db.save(user_id, encrypted)
def get(self, user_id):
encrypted = db.get(user_id)
return self.cipher.decrypt(encrypted).decode()
Validating Tokens
# Validate JWT (ID Token or Access Token)
import jwt
def validate_token(token, jwks_uri, issuer, audience):
# Get signing keys
jwks = requests.get(jwks_uri).json()
# Find key for token
unverified_header = jwt.get_unverified_header(token)
signing_key = None
for key in jwks["keys"]:
if key["kid"] == unverified_header["kid"]:
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
break
if not signing_key:
raise ValueError("No matching signing key")
# Verify and decode
payload = jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=audience,
issuer=issuer
)
return payload
Security Best Practices
OAuth 2.0 Security
# Security recommendations
security_basics:
- "Use HTTPS everywhere"
- "Validate redirect_uri exactly"
- "Use state parameter for CSRF"
- "Use code verifier for PKCE"
token_security:
- "Short-lived access tokens"
- "Long-lived, secure refresh tokens"
- "Never expose tokens in URLs"
- "Implement token revocation"
client_security:
- "Never store client_secret in frontend"
- "Use PKCE for public clients"
- "Validate all responses"
scopes:
- "Request minimum necessary scopes"
- "Don't trust scope parameter in token exchange"
- "Validate granted scopes"
PKCE (Proof Key for Code Exchange)
# PKCE for additional security
class PKCE:
"""PKCE implementation"""
def __init__(self):
self.code_verifier = secrets.token_urlsafe(32)
self.code_challenge = self._generate_challenge()
def _generate_challenge(self):
# SHA256 hash of verifier, base64url encoded
digest = hashlib.sha256(self.code_verifier.encode()).digest()
return base64.urlsafe_b64encode(digest).decode()[:-1]
def get_authorization_params(self):
return {
"code_challenge": self.code_challenge,
"code_challenge_method": "S256"
}
def get_token_params(self, code):
return {
"code_verifier": self.code_verifier
}
Complete Flow with PKCE
# Authorization Code + PKCE
@app.route("/login")
def login():
pkce = PKCE()
session["pkce"] = pkce
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": "openid profile email",
"state": secrets.token_hex(16),
**pkce.get_authorization_params()
}
return redirect(f"{AUTH_URL}?{urllib.parse.urlencode(params)}")
@app.route("/callback")
def callback():
pkce = session["pkce"]
# Exchange code with verifier
token_response = requests.post(
TOKEN_URL,
data={
"grant_type": "authorization_code",
"code": request.args.get("code"),
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
**pkce.get_token_params(request.args.get("code"))
}
).json()
return token_response
Conclusion
OAuth 2.0 and OIDC are essential for modern authentication:
- Authorization Code + PKCE: Best for most applications
- OpenID Connect: Adds authentication layer on OAuth
- Tokens: Short-lived access, long-lived refresh
- Security: PKCE, HTTPS, proper validation
Always use established libraries rather than implementing OAuth yourself.
Comments