py-oidc-auth logo

A small, typed OpenID Connect helper for authentication and authorization.

https://img.shields.io/badge/License-BSD-purple.svg https://readthedocs.org/projects/py-oidc-auth/badge/?version=latest https://codecov.io/gh/freva-org/py-oidc-auth/graph/badge.svg?token=9JP9UWixaf PyPI version Supported Python versions

It provides

  • a framework independent async core: OIDCAuth

  • framework adapters that expose common auth endpoints

  • simple required() and optional() helpers to protect routes

Supported frameworks#

FastAPI

FastAPI#

Flask

Flask#

Quart

Quart#

Tornado

Tornado#

Litestar

Litestar#

Django

Django#

Features#

  • Authorization code flow with PKCE (login and callback)

  • Refresh token flow

  • Device authorization flow

  • Userinfo lookup

  • Provider initiated logout (end session) when supported

  • Bearer token validation using provider JWKS, issuer, and audience

  • Optional scope checks and simple claim constraints

  • Full type annotations

Install#

Pick your framework for installation with pip:

python -m pip install py-oidc-auth[fastapi]
python -m pip install py-oidc-auth[flask]
python -m pip install py-oidc-auth[quart]
python -m pip install py-oidc-auth[tornado]
python -m pip install py-oidc-auth[litestar]
python -m pip install py-oidc-auth[django]

Or with conda/mamba/micromamba:

conda install -c conda-forge py-oidc-auth-fastapi
conda install -c conda-forge py-oidc-auth-flask
conda install -c conda-forge py-oidc-auth-quart
conda install -c conda-forge py-oidc-auth-tornado
conda install -c conda-forge py-oidc-auth-litestar
conda install -c conda-forge py-oidc-auth-django

Import name is py_oidc_auth:

from py_oidc_auth import OIDCAuth

Concepts#

Core#

OIDCAuth is the framework independent client. It loads provider metadata from the OpenID Connect discovery document, performs provider calls, and validates tokens.

Adapters#

Each adapter subclasses OIDCAuth and adds:

  • a method to create a router / blueprint / URL patterns with built-in auth endpoints

  • required() and optional() helpers to validate bearer tokens on protected routes

Default endpoints#

Adapters can expose these paths (customizable and individually disableable):

  • GET  /auth/v2/login

  • GET  /auth/v2/callback

  • POST /auth/v2/token

  • POST /auth/v2/device

  • GET  /auth/v2/logout

  • GET  /auth/v2/userinfo

Adding custom routes#

The router returned by the adapter is a standard framework object. You can add your own endpoints to it before including it in your app. This is useful for exposing application-specific auth metadata alongside the standard OIDC endpoints, for example valid redirect ports for client discovery.

Quick start#

Create one auth instance at app startup, get the router, optionally add custom routes to it, and include it in your app:

from typing import Dict, List, Optional

from fastapi import FastAPI
from py_oidc_auth import FastApiOIDCAuth, IDToken

app = FastAPI()

auth = FastApiOIDCAuth(
    client_id="my-client",
    client_secret="secret",
    discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
    scopes="myscope profile email",
    audience="my-aud",
)

# Get the router and add custom endpoints
auth_router = auth.create_auth_router(prefix="/api")

@auth_router.get("/auth/v2/auth-ports")
async def auth_ports() -> Dict[str, List[int]]:
    return {"valid_ports": [8080, 8443]}

app.include_router(auth_router)

@app.get("/me")
async def me(token: IDToken = auth.required()) -> Dict[str, str]:
    return {"sub": token.sub}

@app.get("/feed")
async def feed(token: Optional[IDToken] = auth.optional()) -> Dict[str, str]:
    return {"authenticated": token is not None}
from flask import Flask, Response, jsonify
from py_oidc_auth import FlaskOIDCAuth, IDToken

app = Flask(__name__)

auth = FlaskOIDCAuth(
    client_id="my-client",
    client_secret="secret",
    discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
    scopes="myscope profile email",
    audience="my-aud",
)

# Get the blueprint and add custom endpoints
auth_bp = auth.create_auth_blueprint(prefix="/api")

@auth_bp.route("/auth/v2/auth-ports")
def auth_ports() -> Response:
    return jsonify({"valid_ports": [8080, 8443]})

app.register_blueprint(auth_bp)

@app.get("/protected")
@auth.required()
def protected(token: IDToken) -> Response:
    return jsonify({"sub": token.sub})
from quart import Quart, Response, jsonify
from py_oidc_auth import QuartOIDCAuth, IDToken

app = Quart(__name__)

auth = QuartOIDCAuth(
    client_id="my-client",
    client_secret="secret",
    discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
    scopes="myscope profile email",
    audience="my-aud",
)

# Get the blueprint and add custom endpoints
auth_bp = auth.create_auth_blueprint(prefix="/api")

@auth_bp.route("/auth/v2/auth-ports")
async def auth_ports() -> Response:
    return jsonify({"valid_ports": [8080, 8443]})

app.register_blueprint(auth_bp)

@app.get("/protected")
@auth.required()
async def protected(token: IDToken) -> Response:
    return jsonify({"sub": token.sub})
from django.http import HttpRequest, JsonResponse
from django.urls import path
from py_oidc_auth import DjangoOIDCAuth, IDToken

auth = DjangoOIDCAuth(
    client_id="my-client",
    client_secret="secret",
    discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
    scopes="myscope profile email",
    audience="my-aud",
)

# Custom endpoint alongside the standard OIDC routes
async def auth_ports(request: HttpRequest) -> JsonResponse:
    return JsonResponse({"valid_ports": [8080, 8443]})

@auth.required()
async def protected_view(request: HttpRequest, token: IDToken) -> JsonResponse:
    return JsonResponse({"sub": token.sub})

urlpatterns = [
    \*auth.get_urlpatterns(),
    path("auth/v2/auth-ports", auth_ports),
    path("protected/", protected_view),
]
import json
import tornado.web
from py_oidc_auth import TornadoOIDCAuth, IDToken

auth = TornadoOIDCAuth(
     client_id="my-client",
     client_secret="secret",
     discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
     scopes="myscope profile email",
     audience="my-aud",
)

# Custom handler alongside the standard OIDC routes
class AuthPortsHandler(tornado.web.RequestHandler):
    def get(self) -> None:
        self.write(json.dumps({"valid_ports": [8080, 8443]}))

class ProtectedHandler(tornado.web.RequestHandler):
    @auth.required()
    async def get(self, token: IDToken) -> None:
        self.write(json.dumps({"sub": token.sub}))

def make_app():
    return tornado.web.Application(
        [
            \*auth.get_auth_routes(prefix="/api"),
            (r"/api/auth/v2/auth-ports", AuthPortsHandler),
            (r"/protected", ProtectedHandler),
        ]
    )
from typing import Dict, List
from litestar import Litestar, get
from py_oidc_auth import LitestarOIDCAuth, IDToken

auth = LitestarOIDCAuth(
     client_id="my-client",
     client_secret="secret",
     discovery_url="https://idp.example.org/realms/demo/.well-known/openid-configuration",
     scopes="myscope profile email",
     audience="my-aud",
)

@get("/auth/v2/auth-ports")
async def auth_ports() -> Dict[str, List[int]]:
    return {"valid_ports": [8080, 8443]}

@get("/protected")
@auth.required()
async def protected(token: IDToken) -> Dict[str, str]:
    return {"sub": token.sub}

app = Litestar(
    route_handlers=[
        auth.create_auth_router(prefix="/api"),
        auth_ports,
        protected,
    ]
)

Scopes audience and claim constraints#

All adapters support:

  • scopes="a b c" to require scopes on a protected endpoint

  • claims={...} to enforce simple claim constraints

  • audience=my-aud to enforce intended audience check

FastAPI Example:

@auth.required(scopes="admin", claims={"groups": ["admins"]})
def admin(token: IDToken) -> Dict[str, str]:
    return {"sub": token.sub}

Documentation#