pygrindvakt¶
pygrindvakt is a Python binding for grindvakt 0.7.x, a runtime-agnostic Rust library for OAuth 2.0, OpenID Connect and OpenID Federation 1.1. It exposes grindvakt’s OpenID Provider engine, its Relying Party toolkit, federation trust-chain resolution, DPoP (RFC 9449) proof validation, and the JOSE / key primitives underneath to Python, including PKCS#11 (HSM) signing.
Important
OAuth 2.0 and OpenID Connect are security protocols, and this binding hands
attacker-controlled request parameters, tokens and JWTs straight into
authentication decisions. Before integrating, read the
security guide: it covers the fail-closed guards
the binding adds on top of grindvakt, the unsafe_* escape hatches, why
token_url must come from configuration and never from the Host
header, and why str(exc) must never be sent to a client.
What the binding adds¶
grindvakt itself is a library of async Rust functions and traits. The
binding turns that into something a Python web application can call directly:
A synchronous API over an embedded tokio runtime. Every networked or store-backed call is driven to completion on a process-wide runtime with the GIL released, so threads run in parallel and the same objects keep working after
fork(gunicorn, uwsgi,multiprocessing). See Stores, workers and the runtime.Framework agnosticism. Requests come in as plain dicts and strings, responses go out as a
pygrindvakt.http.Response(status,headers,body). The samepygrindvakt.provider.Providerworks under Flask, Django, FastAPI, or anything else; the framework adapters are a few lines each (OpenID Provider on Flask, OpenID Provider on Django, OpenID Provider on FastAPI).Python protocol adapters. The client store, token-use store, DPoP replay store and outbound HTTP client can each be a built-in class or any Python object implementing a small duck-typed protocol (Django ORM, redis-py, an
httpxclient with custom CAs). Adapters fail closed: a Python exception is logged throughsys.unraisablehookand reported to the client asserver_error, never as a silent pass.Fail-closed hardening beyond upstream. The binding refuses several things grindvakt silently accepts: an id_token verified without a nonce, duplicate client ids, unknown
Clientfields, reserved id_token claims inextra_claims, and outbound redirects. Each guard is escapable only through an explicit, warning-emittingunsafe_*argument.
The binding mirrors grindvakt’s modules as Python submodules:
Submodule |
Purpose |
|---|---|
Framework-agnostic request / response types, the |
|
Signing keys from PEM / DER, JWK, or a PKCS#11 token; JWK generation and thumbprints. |
|
Registered relying parties and the |
|
The OpenID Provider discovery document. |
|
Parsed OIDC authorization requests. |
|
The stateless token codec and its sealed payloads. |
|
The OpenID Provider engine and the |
|
RFC 9449 DPoP proof validation and the |
|
The Relying Party side: discovery, code exchange, id_token verification, userinfo, client assertions. |
|
OpenID Federation entity statements, trust-chain resolution, signed JWKS, collections, metadata policy. |
|
Home-organization discovery and Third-Party Initiated Login. |
|
Thin JWS sign / verify helpers. |
|
RFC 7636 PKCE helpers. |
|
HMAC-SHA256, SHA-256, constant-time comparison. |
|
Time and randomness helpers. |
Design in one sentence: JSON-shaped values (claims, JWKs, metadata) cross the boundary as native Python dicts and lists, wrapped Rust objects are immutable handles you can share freely across threads, and private key material never crosses into Python at all.
Getting started
Guides
- Security guide
- Fail-closed hardening added by the binding
token_urlcomes from configuration, never fromHost- Never send
str(exc)to a client - The redirect policy
- TLS: rustls, not the system OpenSSL
- Secrets are Python strings
- Input hardening the binding inherits from grindvakt
- Footguns the API leaves reachable (and why)
- Checklist for a production OP
- Checklist for a production RP
- OpenID Provider on Flask
- OpenID Provider on Django
- OpenID Provider on FastAPI
- Relying Party integration
- OpenID Federation
- DPoP: sender-constrained tokens
- Stores, workers and the runtime
API reference
Reference
Architecture decision records¶
The design decisions behind the binding are recorded as Markdown ADRs in the repository rather than in this manual:
ADR 0001: synchronous Python API over an embedded, PID-keyed tokio runtime.
ADR 0002: Python protocol adapters for stores and outbound HTTP, failing closed.
ADR 0003: fail-closed hardening added at the binding boundary.
The full list lives in the docs/adr directory.