OAuth2 Provider for Django web applications with complete server functionality, token management, and authorization endpoints.
—
Django OAuth Toolkit provides comprehensive OpenID Connect 1.0 implementation, adding an identity layer on top of OAuth2. This includes discovery endpoints, UserInfo, JSON Web Key Sets (JWKS), ID tokens, and relying party initiated logout.
OpenID Connect Provider Configuration Information endpoint implementing the discovery specification.
class ConnectDiscoveryInfoView(View):
"""
OIDC discovery endpoint (/.well-known/openid-configuration).
Provides OpenID Provider metadata for client configuration.
Returns JSON with provider capabilities and endpoint URLs.
Methods:
GET: Return OIDC discovery document
Returns:
JSON document with:
- issuer: Provider issuer identifier
- authorization_endpoint: OAuth2 authorization URL
- token_endpoint: OAuth2 token URL
- userinfo_endpoint: OIDC UserInfo URL
- jwks_uri: JSON Web Key Set URL
- end_session_endpoint: Logout URL
- scopes_supported: List of supported OAuth2 scopes
- response_types_supported: Supported OAuth2 response types
- grant_types_supported: Supported OAuth2 grant types
- subject_types_supported: Subject identifier types
- id_token_signing_alg_values_supported: ID token algorithms
- token_endpoint_auth_methods_supported: Client auth methods
"""
def get(self, request, *args, **kwargs):
"""Return OIDC discovery document"""OIDC UserInfo endpoint for retrieving user profile information using access tokens.
class UserInfoView(View):
"""
OIDC UserInfo endpoint (/o/userinfo/).
Returns user profile information for the access token owner.
Requires valid access token with 'openid' scope.
Methods:
GET: Return user information
POST: Return user information (alternative method)
Headers:
Authorization: Bearer ACCESS_TOKEN
Returns:
JSON with user claims:
- sub: Subject identifier (user ID)
- name: Full name
- given_name: First name
- family_name: Last name
- email: Email address
- email_verified: Email verification status
- picture: Profile picture URL
- Additional claims based on scopes and configuration
"""
def get(self, request, *args, **kwargs):
"""Return UserInfo for the token owner"""
def post(self, request, *args, **kwargs):
"""Alternative POST method for UserInfo"""OIDC JWKS endpoint for public key distribution for ID token verification.
class JwksInfoView(View):
"""
OIDC JWKS endpoint (/.well-known/jwks.json).
Provides public keys for verifying ID tokens and other JWTs.
Returns JSON Web Key Set containing cryptographic keys.
Methods:
GET: Return JWKS document
Returns:
JSON Web Key Set with:
- keys: Array of JWK objects containing public keys
- Each key includes: kty, use, kid, n, e (for RSA keys)
- Supports RSA and HMAC signing algorithms
"""
def get(self, request, *args, **kwargs):
"""Return JSON Web Key Set"""OIDC logout endpoint allowing relying parties to initiate user logout.
class RPInitiatedLogoutView(View):
"""
OIDC Relying Party Initiated Logout endpoint (/o/logout/).
Handles logout requests from OIDC clients (relying parties).
Supports both GET and POST methods with logout confirmation.
Methods:
GET: Display logout confirmation form
POST: Process logout confirmation
Query/Form Parameters:
id_token_hint: ID token to identify the user session
logout_hint: Hint about user identity for logout
client_id: OIDC client identifier
post_logout_redirect_uri: Where to redirect after logout
state: Client state parameter
ui_locales: Preferred UI locales
Returns:
Logout confirmation form or redirect to post_logout_redirect_uri
"""
template_name = "oauth2_provider/rp_initiated_logout.html"
form_class = ConfirmLogoutForm
def get(self, request, *args, **kwargs):
"""Display logout confirmation"""
def post(self, request, *args, **kwargs):
"""Process logout confirmation"""OIDC ID token model for storing JWT token metadata and claims.
class IDToken(AbstractIDToken):
"""
OIDC ID token model (already covered in models.md but relevant here).
Stores metadata about issued ID tokens for OIDC flows.
Links to access tokens and provides JWT token identification.
"""
jti: uuid.UUID # JWT Token ID
user: User # Subject user
application: Application # OIDC client
expires: datetime # Token expiration
scope: str # Token scopesURL patterns for OpenID Connect endpoints.
oidc_urlpatterns = [
# OIDC discovery endpoint (supports both with and without trailing slash)
re_path(
r"^\.well-known/openid-configuration/?$",
views.ConnectDiscoveryInfoView.as_view(),
name="oidc-connect-discovery-info",
),
# JWKS endpoint
path(".well-known/jwks.json", views.JwksInfoView.as_view(), name="jwks-info"),
# UserInfo endpoint
path("userinfo/", views.UserInfoView.as_view(), name="user-info"),
# Logout endpoint
path("logout/", views.RPInitiatedLogoutView.as_view(), name="rp-initiated-logout"),
]# Client discovers OIDC provider configuration
# GET /.well-known/openid-configuration
# Response:
# {
# "issuer": "https://example.com",
# "authorization_endpoint": "https://example.com/o/authorize/",
# "token_endpoint": "https://example.com/o/token/",
# "userinfo_endpoint": "https://example.com/o/userinfo/",
# "jwks_uri": "https://example.com/.well-known/jwks.json",
# "end_session_endpoint": "https://example.com/o/logout/",
# "scopes_supported": ["openid", "profile", "email"],
# "response_types_supported": ["code", "id_token", "code id_token"],
# "grant_types_supported": ["authorization_code", "implicit"],
# "subject_types_supported": ["public"],
# "id_token_signing_alg_values_supported": ["RS256", "HS256"],
# "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"]
# }# 1. Authorization request with openid scope
# GET /o/authorize/?response_type=code&scope=openid+profile+email&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&nonce=NONCE&state=STATE
# 2. Token exchange (same as OAuth2 but includes ID token)
# POST /o/token/
# Content-Type: application/x-www-form-urlencoded
#
# grant_type=authorization_code&code=AUTH_CODE&redirect_uri=REDIRECT_URI&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
# Response includes ID token:
# {
# "access_token": "ACCESS_TOKEN",
# "token_type": "Bearer",
# "expires_in": 3600,
# "refresh_token": "REFRESH_TOKEN",
# "scope": "openid profile email",
# "id_token": "ID_TOKEN_JWT"
# }# Request user information with access token
# GET /o/userinfo/
# Authorization: Bearer ACCESS_TOKEN
# Response:
# {
# "sub": "user123",
# "name": "John Doe",
# "given_name": "John",
# "family_name": "Doe",
# "email": "john.doe@example.com",
# "email_verified": true,
# "picture": "https://example.com/avatar.jpg"
# }
# Alternative POST method:
# POST /o/userinfo/
# Content-Type: application/x-www-form-urlencoded
# Authorization: Bearer ACCESS_TOKEN# Get public keys for ID token verification
# GET /.well-known/jwks.json
# Response:
# {
# "keys": [
# {
# "kty": "RSA",
# "use": "sig",
# "kid": "key1",
# "n": "BASE64_MODULUS",
# "e": "AQAB"
# }
# ]
# }# 1. Client initiates logout
# GET /o/logout/?id_token_hint=ID_TOKEN&post_logout_redirect_uri=LOGOUT_URI&state=STATE
# 2. User confirms logout (or automatic if configured)
# POST /o/logout/
# Content-Type: application/x-www-form-urlencoded
#
# allow=true&id_token_hint=ID_TOKEN&post_logout_redirect_uri=LOGOUT_URI&state=STATE
# 3. Redirect to post_logout_redirect_uri
# HTTP/1.1 302 Found
# Location: LOGOUT_URI?state=STATEimport jwt
from oauth2_provider.models import get_application_model
def verify_id_token(id_token_jwt, client_id):
"""Verify and decode OIDC ID token"""
Application = get_application_model()
try:
application = Application.objects.get(client_id=client_id)
# Get signing key
if application.algorithm == Application.RS256_ALGORITHM:
# Use RSA public key
key = application.jwk_key
elif application.algorithm == Application.HS256_ALGORITHM:
# Use client secret
key = application.client_secret
else:
raise ValueError("No signing algorithm configured")
# Decode and verify ID token
payload = jwt.decode(
id_token_jwt,
key,
algorithms=[application.algorithm],
audience=client_id,
issuer="https://example.com" # Your issuer URL
)
return payload
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid ID token: {e}")
except Application.DoesNotExist:
raise ValueError("Unknown client")from oauth2_provider.views.oidc import UserInfoView
from django.http import JsonResponse
class CustomUserInfoView(UserInfoView):
"""Custom UserInfo endpoint with additional claims"""
def get_userinfo_claims(self, token):
"""Add custom claims to UserInfo response"""
claims = super().get_userinfo_claims(token)
# Add custom user attributes
user = token.user
if user:
claims.update({
'custom_field': getattr(user, 'custom_field', None),
'department': getattr(user.profile, 'department', None) if hasattr(user, 'profile') else None,
'roles': list(user.groups.values_list('name', flat=True)),
'last_login': user.last_login.isoformat() if user.last_login else None,
})
return claims
# URL configuration
# path('userinfo/', CustomUserInfoView.as_view(), name='custom-user-info')# settings.py
OAUTH2_PROVIDER = {
# OIDC settings
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': '''-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----''',
# ID token settings
'ID_TOKEN_EXPIRE_SECONDS': 3600,
'OIDC_USERINFO_ENDPOINT_RESPONSE_TYPE': 'application/json',
# Standard OAuth2 scopes plus OIDC scopes
'SCOPES': {
'read': 'Read access',
'write': 'Write access',
'openid': 'OpenID Connect',
'profile': 'User profile information',
'email': 'Email address',
},
# OIDC issuer (your domain)
'OIDC_ISSUER': 'https://yourdomain.com',
}# OIDC Hybrid Flow (code + id_token)
# GET /o/authorize/?response_type=code+id_token&scope=openid+profile&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&nonce=NONCE
# Response includes both authorization code and ID token:
# HTTP/1.1 302 Found
# Location: REDIRECT_URI#code=AUTH_CODE&id_token=ID_TOKEN_JWT&state=STATE
# Client can immediately get user info from ID token
# and exchange code for access token and refresh tokenOIDC endpoints return standard OIDC error responses:
# UserInfo errors:
# HTTP 401 Unauthorized - Invalid or missing access token
# HTTP 403 Forbidden - Token doesn't have 'openid' scope
#
# {
# "error": "invalid_token",
# "error_description": "The access token is invalid"
# }
# Logout errors:
# HTTP 400 Bad Request - Invalid logout request
#
# {
# "error": "invalid_request",
# "error_description": "Missing or invalid id_token_hint"
# }
# Discovery endpoint errors:
# HTTP 500 Internal Server Error - Server configuration issuesInstall with Tessl CLI
npx tessl i tessl/pypi-django-oauth-toolkit