A small, typed OpenID Connect helper for authentication and authorization.
It provides
- a framework independent async core:
OIDCAuth - framework adapters that expose common auth endpoints
- simple
required()andoptional()helpers to protect routes - token minting/brokering and token federation
py-oidc-auth adds OpenID Connect authentication to your Python web application. You create one auth instance at app startup, get a pre-built router (or blueprint / URL patterns), optionally add your own custom routes to it, and include it in your app. Protected routes use required() and optional() helpers.
|
FastAPI |
Flask |
Quart |
|
Tornado |
Litestar |
Django |
- 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 annotation 🏷️
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 use 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-djangoImport name is py_oidc_auth:
from py_oidc_auth import OIDCAuthOIDCAuth is the framework independent client. It loads provider metadata from the
OpenID Connect discovery document, performs provider calls, and validates tokens.
Each adapter subclasses OIDCAuth and adds:
- helpers to register the standard endpoints (router, blueprint, urlpatterns, etc.)
required()andoptional()helpers to validate bearer tokens on protected routes
Adapters can expose these paths (customizable and individually disabled):
GET /auth/v2/loginGET /auth/v2/callbackPOST /auth/v2/tokenPOST /auth/v2/deviceGET /auth/v2/logoutGET /auth/v2/userinfoGET /auth/v2/.well-known/jwks.json
Create one auth instance at app startup:
auth = ...(
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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)from typing import Dict, 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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
app.include_router(auth.create_auth_router(prefix="/api"))
@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]:
if token is None:
message = "Welcome guest"
else:
message = "Welcome back, {token.given_name}"
return {"message": message}from flask import Flask, Response, jsonify
from py_oidc_auth import FlaskOIDCAuth
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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
app.register_blueprint(auth.create_auth_blueprint(prefix="/api"))
@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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
app.register_blueprint(auth.create_auth_blueprint(prefix="/api"))
@app.get("/protected")
@auth.required()
async def protected(token: IDToken) -> Response:
return jsonify({"sub": token.sub})Decorator style:
from django.http import HttpRequest, JsonResponse
from django.urls import include, 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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
@auth.required()
async def protected_view(request: HttpRequest, token: IDToken) -> JsonResponse:
return JsonResponse({"sub": token.sub})
urlpatterns = [
path("api/", include(auth.get_urlpatterns())),
path("protected/", protected_view),
]Routes only:
urlpatterns = [
*auth.get_urlpatterns(prefix="api"),
path("api/", include(...))
]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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
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"/protected", ProtectedHandler),
]
)from typing import Dict
from litestar import Litestar, get
from py_oidc_auth import LitestarOIDCAuth
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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
@get("/protected")
@auth.required()
async def protected(token: IDToken) -> Dict[str, str]:
return {"sub": token.sub}
app = Litestar(
route_handlers=[
protected,
*auth.create_auth_route(prefix="/api"),
]
)All adapters support:
scopes="a b c"to require scopes on a protected endpointclaims={...}to enforce simple claim constraintsaudience=my-audto enforce intended audience check
FastApi Example:
@auth.required(scopes="admin", claims={"groups": ["admins"]})
def admin(token: IDToken) -> Dict[str, str]:
return {"sub": token.sub}The broker_mode=True option allows for the creation of minting of application
specific tokens rather than passing tokens from the Identity Provider.
Token minting also allows for token federation where multiple applications can be configured to trust each others tokens.
- py-oidc-auth-client — typed Python client for authenticating against services that expose py-oidc-auth-compatible routes.
See the CONTRIBUTING.md document to get involved.
