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 routestoken minting/brokering and token federation
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 token minting.
Optional token trust network and token federation.
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()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
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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
# 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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
# 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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
# 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 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"],
)
# 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 = [
path("api/", include(auth.get_urlpatterns())),
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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
# 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",
broker_mode=True,
broker_store_url="postgresql+asyncpg://user:pw@db/myapp",
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
@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 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}
Token minting and federation#
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.
In broker mode the Identity Provider Token must be stored securely server site.
You can choose from a MongoDB, SQLiteDB, PostGresDB or a MySQL/MariaDB. To
configure the token storage you can either use a connection string or create
a py_oidc_auth.broker.store.BrokerStore class from your own Database storage
object (flask example):
from pymongo import AsyncMongoClient
from py_oidc_auth import MongoDBBrokerStore, FlaskOIDCAuth
mongo_client = AsyncMongoClient("mongodb://myser:mypass@host")
auth = FlaskOIDCAuth(
...,
broker_mode=True,
broker_store_obj=MongoDBBrokerStore(db=mongo_client["my-app"]),
broker_audience="myapp-api",
trusted_issuers=["https://other-instance.example.org"],
)
Documentation#
Contents
See also