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
What it does
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.
Supported frameworks
|
FastAPI |
Flask |
Quart |
|
Tornado |
Litestar |
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 annotation 🏷️
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 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-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 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
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",
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"],
)
FastAPI
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}
Flask
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})
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",
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})
Django
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(...))
]
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",
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),
]
)
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",
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"),
]
)
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.
Related
- py-oidc-auth-client — typed Python client for authenticating against services that expose py-oidc-auth-compatible routes.
Contributing
See the CONTRIBUTING.md document to get involved.
Metadata
Release files for py-oidc-auth 2606.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| py_oidc_auth-2606.0.0.tar.gz | 2.8 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_oidc_auth-2606.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.8 MB
Release files / py_oidc_auth-2606.0.0.tar.gz
| Download URL | py_oidc_auth-2606.0.0.tar.gz |
|---|---|
| Size | 2.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
130ee505b6314d230d38ff0d9ec21a17c610269bc6342f2010de0a4630deb130
|
|
BLAKE2b-256 checksum How to use checksums |
4f5661303420248ce0fa298302faea1dda07d81138de9829911bf7b50dab27bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / py_oidc_auth-2606.0.0-py3-none-any.whl
| Download URL | py_oidc_auth-2606.0.0-py3-none-any.whl |
|---|---|
| Size | 59.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
953407d73db95a8cf4f0f5ec1fcf07d3030f1dafd0defd20f4e2a7e2003ee8e4
|
|
BLAKE2b-256 checksum How to use checksums |
1c1dd606c10c78c06927575621dca5d44e265dd6efa473804b508bec1bc45495
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Security for py-oidc-auth 2606.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.