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:
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]
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="openid profile email",
)
FastAPI#
from typing import 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="openid profile email",
)
app.include_router(auth.create_auth_router(prefix="/api"))
@app.get("/me")
async def me(token: IDToken = auth.required()):
return {"sub": token.sub}
@app.get("/feed")
async def feed(token: Optional[IDToken] = auth.optional()):
return {"authenticated": token is not None}
Flask#
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",
)
app.register_blueprint(auth.create_auth_blueprint(prefix="/api"))
@app.get("/protected")
@auth.required()
def protected(token: IDToken) -> Response:
return jsonify({"sub": token.sub})
Quart#
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",
)
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})
Django#
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",
)
@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"),
]
Tornado#
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",
)
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),
]
)
Litestar#
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",
)
@get("/protected")
@auth.required()
async def protected(token: IDToken) -> Dict[str, str]:
return {"sub": token.sub}
app = Litestar(
route_handlers=[
protected,
*auth.get_route_handlers(prefix="/api"),
]
)
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