Authlib - Authentication OAUTH and Openid

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

  1. Always use HTTPS in production to protect tokens and personal data
  2. Store secrets in environment variables, never commit them to source control
  3. Validate the state parameter to prevent CSRF attacks
  4. Use short‑lived access tokens (15–60 minutes)
  5. 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.

News