CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-oauth-toolkit

OAuth2 Provider for Django web applications with complete server functionality, token management, and authorization endpoints.

Pending
Overview
Eval results
Files

oidc.mddocs/

OpenID Connect (OIDC) Support

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.

Capabilities

OIDC Discovery Endpoint

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"""

UserInfo Endpoint

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"""

JSON Web Key Set (JWKS) Endpoint

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"""

Relying Party Initiated Logout

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"""

ID Token Model

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 scopes

OIDC URL Patterns

URL 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"),
]

Usage Examples

OIDC Discovery

# 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"]
# }

OIDC Authorization Flow

# 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"
# }

UserInfo Endpoint Usage

# 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

JWKS Endpoint

# 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"
#     }
#   ]
# }

Logout Flow

# 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=STATE

ID Token Handling

import 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")

Custom UserInfo Claims

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')

OIDC Settings Configuration

# 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',
}

Hybrid Flow Example

# 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 token

Error Handling

OIDC 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 issues

Install with Tessl CLI

npx tessl i tessl/pypi-django-oauth-toolkit

docs

drf-integration.md

index.md

management-commands.md

management-views.md

models.md

oauth2-endpoints.md

oidc.md

settings.md

view-protection.md

tile.json