oidc-caep-risc
A confidential OpenID Connect relying party for a traditional Python application, and a CAEP and RISC receiver on one Shared Signals poll stream. Authorization Code with PKCE (S256) is required. Client authentication is client_secret_basic, client_secret_post, or private_key_jwt. A public client (tokenEndpointAuthMethod "none") is refused. The only cryptographic dependency is cryptography.
JSON configuration keys are the same names the Node library oidc-caep-risc-nodejs reads. signals omitted or true keeps CAEP and RISC on. signals: false does not start the receiver, does not require transmitter metadata, and drops ssf.read and ssf.manage from the scope list after the default scopes are applied. DEFAULT_SCOPES still names those two scopes. The shipped demo leaves signals on (the key is omitted).
The license is MIT. develop and main are the same commit. A pull request merged into main publishes the package to PyPI after the test workflow succeeds on that commit. A direct push does not. There is no API token: PyPI Trusted Publishing trades the workflow's GitHub OIDC token for a short-lived upload token, from the pypi environment, and PyPI holds the publisher for this repository and publish.yml. A version already on PyPI stays there; the next release is a new version in pyproject.toml (and VERSION).
Usage
from oidc_caep_risc import RelyingParty
rp = RelyingParty.from_file("oidc.json")
request = rp.authorize()
# redirect the browser to request["url"]; keep state, nonce, and code_verifier
who = rp.complete(query, request)
Tokens stay in this process. Keep the transaction until the browser returns.
Settings and defaults
The JSON keys are the same in all seven oidc-caep-risc libraries. The same defaults and rules apply whether the client is read from a file (RelyingParty.from_file, resolve_config) or built directly (RelyingParty.create(dict), RelyingParty(config, metadata)).
| Key | Default | Meaning |
|---|---|---|
par |
auto |
Pushed authorization requests (RFC 9126). auto pushes when the provider has a PAR endpoint, always fails at creation without one, never does not push. When discovery says require_pushed_authorization_requests: true, never fails at creation and auto always pushes. |
dpop |
auto |
DPoP (RFC 9449). auto uses DPoP when the provider advertises ES256 in dpop_signing_alg_values_supported; always fails at creation when it does not; never sends no proofs. |
dpopKey |
none | A PEM P-256 private key (or file:, env:, a path) for an application that runs several processes. Without it the client generates an ES256 key pair when it is created and holds it in memory only. |
acrValues |
none | acr_values for every authorization request (a string or a list). authorize(acr_values=...) overrides it. When either names values, the ID token from the code exchange must carry acr, and it must be one of them. The refresh path does not re-check acr. |
idTokenSigningAlgs |
["RS256"] |
The ID token algorithm allow-list. Only algorithms the provider also advertises remain, and none left fails at creation. none is never accepted. An HMAC algorithm (HS256, HS384, HS512) is accepted only when named here. |
idTokenSignedResponseAlg |
none | One pinned algorithm. It replaces the list and must be advertised. |
requireIdTokenTypAndAtHash |
true |
The ID token header must carry typ JWT or application/jwt (case-insensitive), and when the token response carries an access token the ID token must carry a matching at_hash. false checks both only when present. |
clockToleranceSec |
30 |
Skew allowed on exp, nbf and iat. An iat more than this in the future is refused. |
maxIdTokenAgeSec |
600 |
The oldest iat accepted. |
redirectUri, postLogoutRedirectUri, and a per-request authorize(redirect_uri=...) must be https, or http on a loopback host (localhost, 127.0.0.1, [::1]). allowInsecure does not relax that rule; it only lets the provider's own endpoints use http.
DPoP
With DPoP on, a proof (typ dpop+jwt, the public jwk, jti, htm, htu without query or fragment, iat) goes on every request to the PAR endpoint and the token endpoint (code, refresh and client-credentials grants), and on every resource request made with a DPoP-bound token: userinfo(), resource_request(), and every call the Shared Signals receiver makes to the transmitter. A resource proof carries ath. The authorization request carries dpop_jkt (in the PAR body when PAR is used). A 400 use_dpop_nonce from the authorization server, or a 401 WWW-Authenticate: DPoP error="use_dpop_nonce" from a resource server, is retried once with the nonce the response gave, and the latest nonce per server origin goes on the next proof. When a proof was sent the token response's token_type must be DPoP; when none was, it must be Bearer (both case-insensitive).
rp.userinfo(login["access_token"], login["subject"]) # DPoP or Bearer, as the token is
answer = rp.resource_request("GET", "https://api.example/x", token["access_token"], token["token_type"])
Give SignalsReceiver the client with relying_party=rp (and scope, default ssf.manage). It then calls the transmitter with the client-credentials token, a DPoP proof on each call when that token is DPoP-bound, and removes the tokens of each session an event ends from the session token cache. headers= (a callable returning the authorization header) still works for a Bearer token from elsewhere.
Sign-in tokens per session
login = rp.complete(query, request, session_id=sid) # or rp.remember_session(sid, login)
token = rp.access_token_for(sid) # {"access_token", "token_type", "expires_at"}
rp.forget_session(sid) # sign-out
access_token_for returns the cached access token while it has more than 30 seconds left; otherwise it refreshes with the refresh token (with a DPoP proof when DPoP is in use) and updates the cache. A failed refresh, or an expired token with no refresh token, removes the entry and raises OidcError. The default store is MemorySessionTokenStore. Pass session_store= to create or from_file with a SessionTokenStore (get, set, delete; JSON-serializable entries) to share entries across processes. The entries hold tokens; a shared store must keep them confidential.
Tests
docker build --target test .
The suite runs in a container.
The tests build an in-process stub provider and a stub transmitter. They do not start iya-sts.
python3 scripts/live_iya_sts.py is the live half. It starts the latest main branch of iya-sts in memory mode with persistence and clustering off, runs the install script in a scratch copy of this checkout (which writes the tlsCa chain), then builds this demo image from it, checks the image carries the chain, starts the demo with no CA mount and no refresh, signs in, and requires a credential-change event and a session-revoked event on GET /live-status. The second event ends the local session. It binds two high ports, never 3000 or 8081. The nightly workflow runs the unit suite and this script once every 24 hours.
Layout
| Path | What it is |
|---|---|
src/oidc_caep_risc |
The library. relying_party.py, client_auth.py, id_token.py, pkce.py, discovery.py, config.py, dpop.py, session_cache.py, ssf/. |
tests |
pytest ports of the Node relying-party and Shared Signals tests. |
demo |
demo/app.py, a stdlib http.server application. Config is demo/oidc.json. |
scripts/configure_iya_sts.py |
Registers the confidential client on iya-sts and creates the test user oidc-demo-user. A generated client secret goes to the file secretFile names, and a generated password to demo/generated/oidc-demo-user.password, each mode 0600; the script prints neither. Loopback redirects follow the running container's PORT, then the shell PORT. |
Dockerfile, docker-compose.yml |
The demo image. Compose uses host networking. |
The iya-sts trust chain (tlsCa)
tlsCa in demo/oidc.json names the iya-sts TLS listener CA trust chain the library trusts. The default is generated/idp-ca.pem, relative to demo/. The value may also be file:<path>, env:<NAME>, or the PEM itself. The library adds every certificate in it to the default trust store.
scripts/configure_iya_sts.py, the install script, writes that file. It saves the whole chain iya-sts publishes at GET /tls/server-certificate (leaf, TLS Issuing CA, Intermediate, Root), or the chain from the TLS handshake when that path does not answer. It prints the path, the trust anchor's subject and its SHA-256 fingerprint. Compare the anchor with the Root CA on iya-sts's PKI page.
The demo image build copies that file into the image at /app/demo/generated/idp-ca.pem, so the image trusts that iya-sts with no bind mount and no fetch. Run the install script before you build the image. Rebuild the image after running the install script against a different iya-sts. .dockerignore admits only that file from demo/generated. The client secret stays out of the image. A build with no chain file yet (a fresh clone, CI) still succeeds; the image then has no tlsCa file.
The container entrypoint also refreshes the file from the provider at start, because a development iya-sts makes a new CA when it restarts. A failed refresh keeps the file from the build. TLS_CA_REFRESH=0 turns the refresh off. python scripts/trust_chain.py --config demo/oidc.json refreshes it by hand.
Demo
PORT defaults to 3000. A loopback redirect and post-logout URL follow PORT. Any other host is left as written. An explicit PUBLIC_URL or --public-url replaces the origin and keeps the path. The default http://localhost:3000 is not an explicit public URL.
docker compose up --build
GET /healthz returns ok. GET / is the page. GET /login starts authorize and redirects. GET /callback completes the code exchange and keeps the tokens in the library's session token cache under the local session id. GET / asks access_token_for for that session, which refreshes when the token is nearly expired and ends the local session when it cannot. POST /logout signs the local session out and clears its cache entry. GET /live-status is JSON: signedIn, subject, tokenType (DPoP against an iya-sts that advertises it), the receiver state, and the Shared Signals events this process has accepted. The receiver is given the relying party, so its calls to the transmitter carry DPoP proofs, and a session-revoked event clears the session's tokens.
scripts/configure_iya_sts.py registers the client with id_token_signed_response_alg RS256 (oauthIdTokenSignedResponseAlg), the library's default allow-list.
docker-compose.yml sets network_mode: host. The image sets ENV PORT=3000. The health check calls http://127.0.0.1:${PORT}/healthz. The running container's PORT wins over the shell's when scripts/configure_iya_sts.py registers the application.
Demo JSON uses file: and the configuration's own placeholders. It does not contain a generated secret or key.
Metadata
Release files for oidc-caep-risc 0.2.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 | Actions |
|---|---|---|---|---|
| oidc_caep_risc-0.2.0.tar.gz | 55.7 kB | With provenance | Details for oidc_caep_risc-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| oidc_caep_risc-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for oidc_caep_risc-0.2.0-py3-none-any.whl |
Total release size: 99.4 kB
Release files / oidc_caep_risc-0.2.0.tar.gz
| Download URL | oidc_caep_risc-0.2.0.tar.gz |
|---|---|
| Size | 55.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6274084895a272f05a6d1404f2af09efaf7fdd1dcbddcc38bf9757ee9cf81faf
|
|
BLAKE2b-256 checksum How to use checksums |
d0a8baff90f7bc082d5caf10852499c592a973a3d5e9a2f2bd67b534e1d4e737
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / oidc_caep_risc-0.2.0-py3-none-any.whl
| Download URL | oidc_caep_risc-0.2.0-py3-none-any.whl |
|---|---|
| Size | 43.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0fc569f671cf28051bbafcd4519366b0c07b4833e9a08e487d69063c7a1a5e77
|
|
BLAKE2b-256 checksum How to use checksums |
383966a06682efa3385db3e8029f43ce6e338577acf39431f3754ad47e9546a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logSecurity for oidc-caep-risc 0.2.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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for oidc_caep_risc-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logA 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.