Introduction
Modern web applications and APIs require a reliable and flexible authentication system. OAuth 2.0 and OpenID Connect have become the de‑facto standards for this purpose. However, implementing them demands strict adherence to many security nuances. The Authlib library was created to simplify this process in Python.
Authlib – a universal library for OAuth and OpenID Connect authentication that enables fast and secure integration of third‑party authentication services, as well as the creation of your own OAuth server. In this article we’ll explore how to use Authlib in practice, what features it offers, and how to avoid common pitfalls.
OAuth and OpenID Connect Basics
Protocol Fundamentals
OAuth 2.0 is an authorization protocol that allows an application to access a user’s resources without handling their login and password. Instead, temporary access tokens are used, which can be limited by time and scope.
OpenID Connect (OIDC) is an extension of OAuth 2.0 that adds an authentication layer. With OIDC you can not only obtain access to resources but also verify the user’s identity and retrieve profile information.
Difference Between OAuth and OpenID Connect
OAuth 2.0 lets you access third‑party APIs (e.g., list files in Google Drive, post to social networks) but does not provide user information.
OpenID Connect returns structured user data (email, name, profile picture, etc.) via ID tokens and the userinfo endpoint.
Key Roles in OAuth 2.0
- Resource Owner – the entity that owns the protected resources (usually a user)
- Client – the application requesting access to the resources
- Authorization Server – the server that authenticates the user and issues tokens
- Resource Server – the server hosting the protected resources
Authlib Library Overview
Project History and Goals
Authlib was created by Lepture (Hsiaoming Yang), who also authored several other popular Python libraries. The project launched in 2017 with the goal of delivering a comprehensive OAuth and OpenID Connect solution for the Python ecosystem.
Unlike many other libraries, Authlib offers a single API for both client and server use, plus support for the latest security standards. The library is actively maintained and community‑driven.
Key Features of Authlib
- Full standards support: OAuth 1.0a, OAuth 2.0, OpenID Connect 1.0
- Cryptographic capabilities: JWT, JWS, JWK, JWE
- Ready‑made integrations: Flask, Django, Starlette, FastAPI
- Secure defaults: PKCE, state validation, nonce verification
- Flexibility: ability to build both client‑side and server‑side solutions
Library Architecture
Authlib is built modularly:
- Core – core classes and functions for protocol handling
- Integrations – framework‑specific integrations
- JOSE – JSON Web Token and cryptography utilities
- OAuth1/OAuth2 – OAuth protocol implementations
- OIDC – OpenID Connect extensions
Installing and Basic Configuration of Authlib
Installation via pip
pip install Authlib
Additional Dependencies
Depending on the web framework you use, you may need extra packages:
# For Flask
pip install Authlib[flask]
# For FastAPI/Starlette
pip install Authlib[starlette]
# Full installation with all extras
pip install Authlib[all]
Basic Client Initialization
Simple example of initializing an OAuth2 client:
from authlib.integrations.requests_client import OAuth2Session
client = OAuth2Session(
client_id='your_client_id',
client_secret='your_client_secret',
redirect_uri='https://yourapp.com/auth/callback',
scope='openid profile email'
)
Working with an OAuth Client
Creating the Authorization URL
from authlib.integrations.requests_client import OAuth2Session
client = OAuth2Session(
client_id='your_client_id',
client_secret='your_client_secret',
redirect_uri='https://yourapp.com/callback'
)
# Build the URL to which the user will be redirected
authorization_url, state = client.create_authorization_url(
'https://accounts.google.com/o/oauth2/v2/auth',
access_type='offline', # request a refresh token
prompt='consent'
)
# Store the state value for CSRF protection
# authorization_url contains the redirect URL for the user
Fetching an Access Token
After the user authorizes and returns to your callback URL:
# Full callback URL containing the authorization code
callback_url = 'https://yourapp.com/callback?code=AUTH_CODE&state=STATE'
# Exchange the code for a token
token = client.fetch_token(
'https://oauth2.googleapis.com/token',
authorization_response=callback_url
)
# token now holds access_token, refresh_token (if requested), and other fields
Using the Token for API Calls
# Perform an authenticated request
response = client.get('https://www.googleapis.com/oauth2/v1/userinfo')
user_info = response.json()
print(f"User name: {user_info['name']}")
print(f"Email: {user_info['email']}")
Refreshing the Token
# Refresh the token when it expires
if client.token_expired():
new_token = client.refresh_token(
'https://oauth2.googleapis.com/token',
refresh_token=token['refresh_token']
)
Working with OpenID Connect
Retrieving and Decoding the ID Token
OpenID Connect returns an ID token in JWT format that contains user claims:
from authlib.jose import jwt
from authlib.oidc.core import UserInfo
# Fetch the token (contains id_token)
token = client.fetch_token(
'https://oauth2.googleapis.com/token',
authorization_response=callback_url
)
# Decode the ID token
id_token = token['id_token']
claims = jwt.decode(id_token, key=public_key)
# Validate the claims
claims.validate()
# Build a UserInfo object
user_info = UserInfo(claims)
print(f"User: {user_info['name']}")
print(f"Email verified: {user_info['email_verified']}")
ID Token Validation
from authlib.jose import jwt
from authlib.oidc.core import CodeIDToken
# Retrieve the provider's public key
public_key = get_provider_public_key()
# Create a validator instance
validator = CodeIDToken(
client_id='your_client_id',
issuer='https://accounts.google.com'
)
# Validate the token
claims = validator.validate_token(id_token, public_key, nonce='nonce_value')
Using the UserInfo Endpoint
# Get additional profile data
userinfo_response = client.get('https://www.googleapis.com/oauth2/v1/userinfo')
userinfo = userinfo_response.json()
# Merge with ID token data
from authlib.oidc.core import UserInfo
user = UserInfo(userinfo)
Flask Integration
Full Flask Application Setup
from flask import Flask, redirect, url_for, session, request
from authlib.integrations.flask_client import OAuth
import os
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY') or 'dev-secret-key'
oauth = OAuth(app)
# Register the provider
google = oauth.register(
name='google',
client_id=os.environ.get('GOOGLE_CLIENT_ID'),
client_secret=os.environ.get('GOOGLE_CLIENT_SECRET'),
server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
@app.route('/')
def index():
if 'user' in session:
return f'''
<h1>Welcome, {session["user"]["name"]}!</h1>
<p>Email: {session["user"]["email"]}</p>
<a href="/logout">Log out</a>
'''
return '<a href="/login">Log in with Google</a>'
@app.route('/login')
def login():
redirect_uri = url_for('authorize', _external=True)
return google.authorize_redirect(redirect_uri)
@app.route('/authorize')
def authorize():
token = google.authorize_access_token()
user = google.parse_id_token(token)
session['user'] = user
return redirect(url_for('index'))
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)
Protecting Routes
from functools import wraps
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
@app.route('/protected')
@login_required
def protected():
return f"Protected page for {session['user']['name']}"
FastAPI Integration
Basic FastAPI Setup with Authlib
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import RedirectResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import os
app = FastAPI()
# Add session middleware
app.add_middleware(SessionMiddleware, secret_key=os.environ.get('SECRET_KEY'))
oauth = OAuth()
oauth.register(
name='github',
client_id=os.environ.get('GITHUB_CLIENT_ID'),
client_secret=os.environ.get('GITHUB_CLIENT_SECRET'),
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
api_base_url='https://api.github.com/',
client_kwargs={'scope': 'user:email'},
)
@app.get('/')
async def homepage(request: Request):
user = request.session.get('user')
if user:
return {'message': f'Hi, {user["name"]}!'}
return RedirectResponse(url='/login')
@app.get('/login')
async def login(request: Request):
redirect_uri = request.url_for('auth')
return await oauth.github.authorize_redirect(request, redirect_uri)
@app.get('/auth')
async def auth(request: Request):
try:
token = await oauth.github.authorize_access_token(request)
user_resp = await oauth.github.get('user', token=token)
user_data = user_resp.json()
# Store user data in session
request.session['user'] = {
'name': user_data['name'] or user_data['login'],
'email': user_data['email'],
'avatar': user_data['avatar_url']
}
return RedirectResponse(url='/')
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get('/logout')
async def logout(request: Request):
request.session.pop('user', None)
return RedirectResponse(url='/')
# Dependency for auth checks
async def get_current_user(request: Request):
user = request.session.get('user')
if not user:
raise HTTPException(status_code=401, detail='Authentication required')
return user
@app.get('/protected')
async def protected(user: dict = Depends(get_current_user)):
return {'message': f'Protected data for {user["name"]}'}
Creating Your Own OAuth Server
Basic OAuth Server Structure
from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector
from authlib.oauth2.rfc6749 import grants
from authlib.oauth2.rfc7636 import CodeChallenge
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
def save_authorization_code(self, code, request):
# Persist the authorization code in the DB
pass
def query_authorization_code(self, code, client):
# Retrieve the code from the DB
pass
def delete_authorization_code(self, authorization_code):
# Delete the used code
pass
def authenticate_user(self, authorization_code):
# Authenticate the user based on the code
pass
class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
def authenticate_user(self, username, password):
# Verify username and password
pass
class RefreshTokenGrant(grants.RefreshTokenGrant):
def authenticate_refresh_token(self, refresh_token):
# Validate the refresh token
pass
def authenticate_user(self, credential):
# Authenticate the user via the refresh token
pass
# Server configuration
authorization = AuthorizationServer()
authorization.init_app(app)
# Register grant types
authorization.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)])
authorization.register_grant(PasswordGrant)
authorization.register_grant(RefreshTokenGrant)
OAuth Server Endpoints
@app.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
if request.method == 'GET':
# Show the consent form
try:
grant = authorization.validate_consent_request(end_user=current_user)
return render_template('authorize.html', grant=grant)
except OAuth2Error as error:
return render_template('error.html', error=error)
# POST – user confirmed consent
return authorization.create_authorization_response(end_user=current_user)
@app.route('/oauth/token', methods=['POST'])
def issue_token():
return authorization.create_token_response()
@app.route('/oauth/revoke', methods=['POST'])
def revoke_token():
return authorization.create_revocation_response()
Authlib Methods and Functions Reference
Main Classes and Methods
| Class / Function | Module | Description | Usage Example |
|---|---|---|---|
OAuth2Session |
authlib.integrations.requests_client |
OAuth2 client built on requests |
OAuth2Session(client_id, client_secret) |
OAuth |
authlib.integrations.flask_client |
OAuth client for Flask | oauth = OAuth(app) |
OAuth |
authlib.integrations.starlette_client |
OAuth client for Starlette / FastAPI | oauth = OAuth() |
AuthorizationServer |
authlib.integrations.flask_oauth2 |
OAuth2 server for Flask | server = AuthorizationServer() |
ResourceProtector |
authlib.integrations.flask_oauth2 |
OAuth2 resource protection helper | require_oauth = ResourceProtector() |
OAuth2Session Methods
| Method | Description | Parameters |
|---|---|---|
create_authorization_url() |
Builds the URL for the authorization request | authorization_endpoint, **kwargs |
fetch_token() |
Obtains a token using an authorization code | token_endpoint, authorization_response |
refresh_token() |
Refreshes an expired access token | token_endpoint, refresh_token |
get() |
Performs a GET request with the bearer token | url, **kwargs |
post() |
Performs a POST request with the bearer token | url, **kwargs |
token_expired() |
Checks whether the token has expired | None |
Flask OAuth Methods
| Method | Description | Parameters |
|---|---|---|
register() |
Registers an OAuth provider | name, client_id, client_secret, **kwargs |
authorize_redirect() |
Redirects the user to the provider’s consent page | redirect_uri, **kwargs |
authorize_access_token() |
Retrieves the token after the provider redirects back | **kwargs |
parse_id_token() |
Parses an OpenID Connect ID token | token |
get() |
Executes an authenticated GET request | url, **kwargs |
JOSE Functions
| Function | Module | Description | Parameters |
|---|---|---|---|
jwt.encode() |
authlib.jose |
Encodes a JWT token | header, payload, key |
jwt.decode() |
authlib.jose |
Decodes a JWT token | token, key |
JsonWebKey.generate_key() |
authlib.jose |
Generates a JWK key | kty, crv_or_size, is_private |
jws.serialize() |
authlib.jose |
Serializes a JWS | protected, payload, key |
jwe.encrypt() |
authlib.jose |
Encrypts data using JWE | plaintext, key, **kwargs |
Grant Types for OAuth Servers
| Grant Type | Class | Description |
|---|---|---|
| Authorization Code | AuthorizationCodeGrant |
Standard flow for web applications |
| Client Credentials | ClientCredentialsGrant |
Server‑to‑server authorization |
| Password | ResourceOwnerPasswordCredentialsGrant |
Direct username/password authentication |
| Refresh Token | RefreshTokenGrant |
Access token renewal |
| Implicit | ImplicitGrant |
Simplified flow (generally discouraged) |
Security and Best Practices
Core Security Principles
- Always use HTTPS in production to protect tokens and personal data
- Store secrets in environment variables, never commit them to source control
- Validate the
stateparameter to prevent CSRF attacks - Use short‑lived access tokens (15–60 minutes)
- Implement proper logout with token revocation
Environment Variable Setup
# .env example
SECRET_KEY=your-secret-key-here
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
Token Validation Helpers
from authlib.oauth2.rfc6749.errors import OAuth2Error
import time, logging
logger = logging.getLogger(__name__)
def validate_token(token):
"""Ensures the token is still valid"""
try:
# Check expiration
if token.get('expires_at', 0) < time.time():
raise OAuth2Error('Token expired')
# Verify required scopes
required_scopes = ['openid', 'profile']
token_scopes = token.get('scope', '').split()
if not all(scope in token_scopes for scope in required_scopes):
raise OAuth2Error('Insufficient scope')
return True
except Exception as e:
logger.error(f'Token validation failed: {e}')
return False
PKCE Implementation
from authlib.oauth2.rfc7636 import create_s256_code_challenge
import secrets
# Generate a code verifier
code_verifier = secrets.token_urlsafe(32)
# Create the code challenge
code_challenge = create_s256_code_challenge(code_verifier)
# Use when building the authorization URL
authorization_url, state = client.create_authorization_url(
authorization_endpoint,
code_challenge=code_challenge,
code_challenge_method='S256'
)
# When exchanging the code for a token, send the verifier
token = client.fetch_token(
token_endpoint,
authorization_response=callback_url,
code_verifier=code_verifier
)
Error Handling and Debugging
Common Errors and Solutions
Error: “Invalid client credentials”
# Verify client_id and client_secret
# Ensure redirect_uri matches the one registered with the provider
try:
token = client.fetch_token(token_endpoint, authorization_response=callback_url)
except Exception as e:
logger.error(f'Token fetch failed: {e}')
if 'invalid_client' in str(e):
# Check client credentials
pass
Error: “Token expired”
def refresh_token_if_needed(client, token):
"""Refreshes the token when it has expired"""
if client.token_expired():
try:
new_token = client.refresh_token(
token_endpoint,
refresh_token=token['refresh_token']
)
return new_token
except Exception as e:
logger.error(f'Token refresh failed: {e}')
# Redirect user to re‑authenticate
return None
return token
Error: “Redirect URI mismatch”
# Make sure the redirect_uri in code matches the one configured with the provider
# Verify scheme (http/https), domain, and port
redirect_uri = url_for('callback', _external=True)
# _external=True is required to generate a full URL
Logging and Monitoring
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Log OAuth events
@app.route('/oauth/callback')
def callback():
try:
logger.info('OAuth callback received')
token = oauth.github.authorize_access_token()
logger.info(f'Token received: {token.get("access_token", "")[:10]}...')
return redirect('/')
except Exception as e:
logger.error(f'OAuth callback failed: {e}')
return 'Authorization failed', 400
Testing Applications with Authlib
Mocking OAuth for Unit Tests
import unittest
from unittest.mock import patch, MagicMock
from your_app import app
class TestOAuth(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
self.app.testing = True
@patch('authlib.integrations.flask_client.OAuth.register')
def test_oauth_login(self, mock_register):
# Mock the provider
mock_provider = MagicMock()
mock_provider.authorize_redirect.return_value = 'redirect_response'
mock_register.return_value = mock_provider
response = self.app.get('/login')
self.assertEqual(response.status_code, 200)
@patch('authlib.integrations.flask_client.OAuth.register')
def test_oauth_callback(self, mock_register):
# Mock a successful callback
mock_provider = MagicMock()
mock_provider.authorize_access_token.return_value = {
'access_token': 'test_token',
'token_type': 'Bearer'
}
mock_provider.parse_id_token.return_value = {
'name': 'Test User',
'email': 'test@example.com'
}
mock_register.return_value = mock_provider
response = self.app.get('/callback?code=test_code&state=test_state')
self.assertEqual(response.status_code, 302) # Redirect after success
Integration Tests
from httpx import AsyncClient
import pytest
@pytest.mark.asyncio
async def test_oauth_flow_integration():
"""Integration test for the OAuth flow"""
async with AsyncClient(app=app, base_url="http://test") as client:
# Get the authorization URL
response = await client.get('/login')
assert response.status_code == 302
# Simulate the callback with a code
callback_response = await client.get('/callback?code=test_code&state=test_state')
assert callback_response.status_code == 302
Integration with Popular Providers
Provider‑Specific Configuration
Google OAuth
google = oauth.register(
name='google',
client_id='your-google-client-id',
client_secret='your-google-client-secret',
server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
client_kwargs={
'scope': 'openid email profile',
'prompt': 'consent', # always show consent screen
'access_type': 'offline', # request refresh token
}
)
GitHub OAuth
github = oauth.register(
name='github',
client_id='your-github-client-id',
client_secret='your-github-client-secret',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
api_base_url='https://api.github.com/',
client_kwargs={
'scope': 'user:email read:user'
}
)
VK OAuth
vk = oauth.register(
name='vk',
client_id='your-vk-app-id',
client_secret='your-vk-secret',
access_token_url='https://oauth.vk.com/access_token',
authorize_url='https://oauth.vk.com/authorize',
api_base_url='https://api.vk.com/method/',
client_kwargs={
'scope': 'email',
'response_type': 'code',
'v': '5.131' # API version
}
)
Discord OAuth
discord = oauth.register(
name='discord',
client_id='your-discord-client-id',
client_secret='your-discord-client-secret',
access_token_url='https://discord.com/api/v10/oauth2/token',
authorize_url='https://discord.com/api/oauth2/authorize',
api_base_url='https://discord.com/api/v10/',
client_kwargs={
'scope': 'identify email guilds'
}
)
Fetching Additional Provider Data
@app.route('/profile')
def profile():
if 'user' not in session:
return redirect(url_for('login'))
user = session['user']
provider = user.get('provider')
if provider == 'google':
# Get extra info from Google
resp = google.get('https://www.googleapis.com/oauth2/v1/userinfo')
user_data = resp.json()
elif provider == 'github':
# Get info from GitHub
resp = github.get('user')
user_data = resp.json()
# Fetch email separately (may be private)
emails_resp = github.get('user/emails')
emails = emails_resp.json()
primary_email = next(email['email'] for email in emails if email['primary'])
return render_template('profile.html', user=user_data)
Advanced Features
Working with JWT Tokens
from authlib.jose import jwt, JsonWebKey
import time, logging
logger = logging.getLogger(__name__)
def create_jwt_token(user_id, expires_in=3600):
header = {'alg': 'RS256'}
payload = {
'user_id': user_id,
'exp': int(time.time()) + expires_in,
'iat': int(time.time()),
'iss': 'your-app.com'
}
# Load private key
with open('private_key.pem', 'rb') as f:
private_key = f.read()
token = jwt.encode(header, payload, private_key)
return token
def verify_jwt_token(token):
try:
# Load public key
with open('public_key.pem', 'rb') as f:
public_key = f.read()
payload = jwt.decode(token, public_key)
payload.validate()
return payload
except Exception as e:
logger.error(f'JWT verification failed: {e}')
return None
Working with JWK (JSON Web Keys)
from authlib.jose import JsonWebKey, KeySet
def generate_jwk_pair():
"""Generates a private/public JWK pair"""
private_key = JsonWebKey.generate_key(
kty='RSA',
crv_or_size=2048,
is_private=True
)
public_key = private_key.get_public_key()
return private_key, public_key
def create_jwk_set(keys):
"""Creates a JWK Set for publishing"""
keyset = KeySet()
for key in keys:
keyset.add(key)
return keyset.as_dict()
@app.route('/.well-known/jwks.json')
def jwks():
"""Publishes the JWK Set"""
keys = load_public_keys() # Implement key loading
jwk_set = create_jwk_set(keys)
return jsonify(jwk_set)
Custom Claims in JWT
def create_custom_jwt(user, permissions=None):
"""Creates a JWT with custom claims"""
payload = {
'sub': str(user.id),
'name': user.name,
'email': user.email,
'email_verified': user.email_verified,
'iat': int(time.time()),
'exp': int(time.time()) + 3600,
'iss': 'your-app.com',
'aud': 'your-app-client',
# Custom claims
'permissions': permissions or [],
'user_role': user.role,
'org_id': user.organization_id,
'custom_data': {
'theme': user.preferences.get('theme', 'light'),
'language': user.preferences.get('language', 'en')
}
}
return jwt.encode(header, payload, private_key)
Performance and Optimization
Token Caching
from functools import lru_cache
import redis, json
# Redis client
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def cache_token(user_id, token):
"""Caches a token in Redis"""
redis_client.setex(
f'token:{user_id}',
3600, # TTL in seconds
json.dumps(token)
)
def get_cached_token(user_id):
"""Retrieves a token from cache"""
cached = redis_client.get(f'token:{user_id}')
if cached:
return json.loads(cached)
return None
# In‑memory caching of provider metadata
@lru_cache(maxsize=1000)
def get_provider_metadata(provider_url):
"""Caches provider metadata"""
response = requests.get(provider_url)
return response.json()
Asynchronous Processing
import asyncio, aiohttp
from authlib.integrations.httpx_client import AsyncOAuth2Client
async def async_oauth_flow():
"""Asynchronous OAuth flow example"""
async with AsyncOAuth2Client(
client_id='client_id',
client_secret='client_secret'
) as client:
# Fetch token
token = await client.fetch_token(
token_endpoint,
authorization_response=callback_url
)
# Perform API request
async with aiohttp.ClientSession() as session:
headers = {'Authorization': f'Bearer {token["access_token"]}'}
async with session.get('https://api.example.com/user', headers=headers) as resp:
user_data = await resp.json()
return user_data
Monitoring and Metrics
OAuth Event Logging
import structlog
from datetime import datetime
logger = structlog.get_logger()
class OAuthEventLogger:
@staticmethod
def log_authorization_start(user_id, provider):
logger.info(
"oauth_authorization_started",
user_id=user_id,
provider=provider,
timestamp=datetime.utcnow().isoformat()
)
@staticmethod
def log_authorization_success(user_id, provider, token_info):
logger.info(
"oauth_authorization_success",
user_id=user_id,
provider=provider,
token_type=token_info.get('token_type'),
expires_in=token_info.get('expires_in'),
timestamp=datetime.utcnow().isoformat()
)
@staticmethod
def log_authorization_failure(user_id, provider, error):
logger.error(
"oauth_authorization_failed",
user_id=user_id,
provider=provider,
error=str(error),
timestamp=datetime.utcnow().isoformat()
)
Prometheus Metrics
from prometheus_client import Counter, Histogram, start_http_server
import time
# OAuth metrics
oauth_requests_total = Counter(
'oauth_requests_total',
'Total OAuth requests',
['provider', 'status']
)
oauth_request_duration = Histogram(
'oauth_request_duration_seconds',
'OAuth request duration',
['provider', 'endpoint']
)
def track_oauth_request(provider, endpoint):
"""Decorator to track OAuth request metrics"""
def decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
oauth_requests_total.labels(provider=provider, status='success').inc()
return result
except Exception:
oauth_requests_total.labels(provider=provider, status='error').inc()
raise
finally:
oauth_request_duration.labels(provider=provider, endpoint=endpoint).observe(
time.time() - start_time
)
return wrapper
return decorator
Frequently Asked Questions
What is Authlib and how does it differ from other libraries?
Authlib is a comprehensive library for OAuth 1.0a, OAuth 2.0, and OpenID Connect in Python. It stands out by offering a unified API for both client‑side and server‑side implementations, supporting the latest security standards, and providing built‑in integrations with popular web frameworks.
Can I use Authlib with FastAPI?
Yes. Authlib includes a dedicated integration for Starlette, which works seamlessly with FastAPI. Use authlib.integrations.starlette_client.OAuth to create OAuth clients in FastAPI applications.
Does Authlib support OpenID Connect?
Absolutely. Authlib fully supports OpenID Connect, including ID token handling, claims extraction, the UserInfo endpoint, and OpenID Discovery. The library automatically manages OIDC‑specific details.
Is Authlib safe for production?
Yes. Authlib is built with security in mind and implements modern protection mechanisms such as PKCE, state validation, and nonce verification. Production safety still requires best practices: enforce HTTPS, store secrets securely, and validate tokens.
How do I configure an OAuth client for Google?
Register your application in Google Cloud Console, obtain a client ID and client secret, set the allowed redirect URIs, and then use the server_metadata_url to auto‑configure the provider.
Can I build my own OAuth server with Authlib?
Yes. Authlib provides a full toolkit for creating a custom OAuth 2.0 server, including support for multiple grant types, client management, token issuance, and validation.
How do I refresh an expired token?
Authlib automatically handles token refresh when you have a refresh token. Call refresh_token() or check token_expired() and refresh as needed.
Does Authlib support asynchronous operation?
Yes. Authlib offers async support through the httpx client (AsyncOAuth2Client) and integrates with async frameworks like Starlette and FastAPI.
How should I test OAuth‑enabled applications?
Use mock objects to simulate providers, generate test tokens, employ test clients, or configure sandbox environments offered by the OAuth providers.
Which providers are compatible with Authlib?
Authlib works with any provider that complies with OAuth 2.0 or OpenID Connect specifications. Popular choices include Google, GitHub, Facebook, Twitter, Microsoft, Auth0, Keycloak, and many more.
Comparison with Other Libraries
| Library | OAuth 1.0a | OAuth 2.0 | OpenID Connect | Server | Client | Integrations |
|---|---|---|---|---|---|---|
| Authlib | ✅ | ✅ | ✅ | ✅ | ✅ | Flask, FastAPI, Django |
| requests‑oauthlib | ✅ | ✅ | ❌ | ❌ | ✅ | requests only |
| python‑social‑auth | ❌ | ✅ | Partial | ❌ | ✅ | Django, Flask |
| oauthlib | ✅ | ✅ | ❌ | ✅ | ✅ | Low‑level |
| flask‑oauthlib | ✅ | ✅ | ❌ | ✅ | ✅ | Flask only |
Authlib Advantages
- Modernity: supports the latest specs and extensions
- Universality: single API for both client and server
- Security: built‑in PKCE, state validation, nonce handling
- Flexibility: easy to customize and extend
- Documentation: thorough docs and practical examples
When to Choose Authlib
- You need OpenID Connect support
- You plan to build a custom OAuth server
- Maximum security is a priority
- You use modern frameworks (FastAPI, recent Flask)
- You require flexible configuration and extensibility
Practical Usage Examples
Microservice Architecture
# Authentication service
class AuthService:
def __init__(self):
self.oauth_server = AuthorizationServer()
self.resource_protector = ResourceProtector()
def validate_token(self, token):
"""Validate token for other services"""
try:
claims = jwt.decode(token, public_key)
return claims
except Exception:
return None
def create_service_token(self, service_name, scopes):
"""Create a token for inter‑service communication"""
payload = {
'sub': service_name,
'aud': 'internal-api',
'scopes': scopes,
'exp': int(time.time()) + 3600
}
return jwt.encode(header, payload, private_key)
# Middleware for token validation
class TokenValidationMiddleware:
def __init__(self, app, auth_service):
self.app = app
self.auth_service = auth_service
def __call__(self, environ, start_response):
auth_header = environ.get('HTTP_AUTHORIZATION')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header[7:]
claims = self.auth_service.validate_token(token)
if claims:
environ['user'] = claims
return self.app(environ, start_response)
SPA (Single Page Application) Integration
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=['http://localhost:3000']) # React front‑end
@app.route('/api/auth/login', methods=['POST'])
def api_login():
"""API endpoint for SPA login"""
data = request.json
provider = data.get('provider')
if provider == 'google':
authorization_url, state = google.create_authorization_url(
'https://accounts.google.com/o/oauth2/v2/auth',
access_type='offline',
prompt='consent'
)
# Store state in session or Redis
session['oauth_state'] = state
return jsonify({
'authorization_url': authorization_url,
'state': state
})
@app.route('/api/auth/callback', methods=['POST'])
def api_callback():
"""Handle provider callback for SPA"""
data = request.json
authorization_response = data.get('authorization_response')
try:
token = google.fetch_token(
'https://oauth2.googleapis.com/token',
authorization_response=authorization_response
)
# Issue a JWT for the SPA
user_token = create_jwt_token(token['access_token'])
return jsonify({
'success': True,
'token': user_token,
'expires_in': 3600
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 400
Mobile App with PKCE
from authlib.oauth2.rfc7636 import create_s256_code_challenge
import secrets
@app.route('/api/mobile/auth/start', methods=['POST'])
def mobile_auth_start():
"""Start mobile PKCE flow"""
# Generate PKCE parameters
code_verifier = secrets.token_urlsafe(32)
code_challenge = create_s256_code_challenge(code_verifier)
# Build authorization URL with PKCE
authorization_url, state = oauth_client.create_authorization_url(
'https://provider.com/oauth/authorize',
code_challenge=code_challenge,
code_challenge_method='S256'
)
# Store code_verifier securely (e.g., Redis)
redis_client.setex(f'pkce:{state}', 600, code_verifier)
return jsonify({
'authorization_url': authorization_url,
'state': state
})
@app.route('/api/mobile/auth/token', methods=['POST'])
def mobile_auth_token():
"""Exchange code for token in mobile flow"""
data = request.json
code = data.get('code')
state = data.get('state')
# Retrieve the stored verifier
code_verifier = redis_client.get(f'pkce:{state}')
if not code_verifier:
return jsonify({'error': 'Invalid state'}), 400
try:
token = oauth_client.fetch_token(
'https://provider.com/oauth/token',
code=code,
code_verifier=code_verifier.decode()
)
return jsonify({
'access_token': token['access_token'],
'refresh_token': token.get('refresh_token'),
'expires_in': token.get('expires_in')
})
except Exception as e:
return jsonify({'error': str(e)}), 400
Migrating from Other Libraries
Migrating from requests‑oauthlib
# Old (requests‑oauthlib)
from requests_oauthlib import OAuth2Session
google = OAuth2Session(
client_id,
scope=['openid', 'email', 'profile'],
redirect_uri=redirect_uri
)
# New (Authlib)
from authlib.integrations.requests_client import OAuth2Session
google = OAuth2Session(
client_id,
client_secret,
scope='openid email profile',
redirect_uri=redirect_uri
)
Migrating from python‑social‑auth
# Old (python‑social‑auth)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'your-key'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'your-secret'
# New (Authlib + Flask)
oauth = OAuth(app)
google = oauth.register(
name='google',
client_id='your-key',
client_secret='your-secret',
server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
client_kwargs={'scope': 'openid email profile'}
)
Conclusion
Authlib is a powerful, modern solution for implementing authentication and authorization in Python applications. It provides a full suite of tools for OAuth 1.0a, OAuth 2.0, and OpenID Connect, supporting both client‑side and server‑side scenarios.
Key benefits of Authlib include:
- Comprehensiveness: covers all current authentication standards
- Security: built‑in protection against common vulnerabilities
- Flexibility: adaptable to a wide range of use cases
- Integrations: ready‑made adapters for popular web frameworks
- Active development: frequent updates and support for new specs
The library is ideal for modern web apps, APIs, microservices, and mobile applications. Thanks to thorough documentation and an active community, Authlib is becoming the de‑facto standard for OAuth implementations in the Python ecosystem.
If you need to quickly add third‑party login, build a custom OAuth server, or secure a microservice architecture, Authlib is an excellent choice for your project.
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed