A small, typed OpenID Connect helper for authentication and authorization.
It provides
a framework independent async core:
OIDCAuthframework adapters that expose common auth endpoints
simple
required()andoptional()helpers to protect routes
Supported frameworks#
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-litestart
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:
helpers to register the standard endpoints (router, blueprint, urlpatterns, etc.)
required()andoptional()helpers to validate bearer tokens on protected routes
Default endpoints#
Adapters can expose these paths (customizable and individually disableable):
GET /auth/v2/loginGET /auth/v2/callbackPOST /auth/v2/tokenPOST /auth/v2/deviceGET /auth/v2/logoutGET /auth/v2/userinfo
Quick start#
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",
)
from typing import Dict, Optional
from fastapi import FastAPI
from py_oidc_auth import FastApiOIDCAuth
from py_oidc_auth.schema import 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",
)
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]:
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",
)
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",
)
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 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",
)
@auth.required()
async def protected_view(request: HttpRequest, token: IDToken) -> JsonResponse:
return JsonResponse({"sub": token.sub})
urlpatterns = [
\*auth.get_urlpatterns(prefix="api"),
path("protected/", protected_view),
]
Routes only:
urlpatterns = [
\*auth.get_urlpatterns(prefix="api"),
]
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",
)
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_handlers(prefix="/api") + [
(r"/protected", ProtectedHandler),
]
)
Scopes and claim constraints#
All adapters support:
scopes="a b c"to require scopes on a protected endpointclaims={...}to enforce simple claim constraints
FastAPI Example:
@auth.required(scopes="admin", claims={"groups": ["admins"]})
def admin(token: IDToken) -> Dict[str, str]:
return {"sub": token.sub}
Documentation#
Contents
See also