wsfed-caep
Server-side WS-Federation relying party for Python. It accepts SAML 2.0 bearer assertions, reads identity-provider federation metadata, writes relying-party metadata, verifies XML signatures, decrypts encrypted assertions, and remembers assertion ids so a bearer token cannot be replayed. A Shared Signals poll receiver for CAEP and RISC is included. The relying party does not start that receiver.
Version 0.1.0. License: MIT. 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). develop and main are the same commit when this tree is published; nothing here creates a remote.
Usage
from wsfed_caep import RelyingParty
rp = RelyingParty.from_file("wsfed.json")
request = rp.login(relay_state="/")
who = rp.handle_reply({"method": "POST", "url": "/wsfed", "body": body}, request.id)
JSON keys match the Node configuration: realm, replyUrl, idpMetadata, privateKey, certificate, idpCa, idpEntityId, clockSkewSeconds, wantAssertionsEncrypted, allowUnsolicited, requestTokenType, signals, ssf. file: and env: prefixes are accepted. The default subject attribute is http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier.
signals omitted or true is on. The resolved configuration omits the key in that case. false is kept, does not start a receiver, and does not require transmitter metadata. Sign-in is unchanged either way: WS-Federation does not send OAuth scopes. Any other value raises a config error whose message is signals must be true or false.
docker compose build
docker run --rm --user "$(id -u):$(id -g)" \
-v "$PWD/demo:/app/demo" \
wsfed-caep-python-demo \
wsfed-caep generate-keys --config demo/wsfed.json
docker run --rm --user "$(id -u):$(id -g)" \
-v "$PWD/demo:/app/demo" \
wsfed-caep-python-demo \
wsfed-caep metadata --config demo/wsfed.json
The generated certificate's common name defaults to wsfed-caep-nodejs relying party plus the realm, cut at 64 characters.
Tests
docker build --target test .
The suite runs in a container.
The suite includes a stub identity provider. Those tests do not start iya-sts.
python3 scripts/live_iya_sts.py is the live half. It uses iya-sts (the latest main branch, in memory mode with persistence and clustering off, when IYA_STS_URL is unset; IYA_STS_IMAGE starts a fresh container from an image), copies this checkout to a scratch build context, builds the demo image, runs the install script inside it (which registers the demo, generates the key pair and writes the idpCa chain), then builds the demo image again so it carries the chain, checks the image holds the chain and no key, starts the demo with no CA mount, signs in through GET /login, and requires a credential-change event and a session-revoked event on GET /live-status. The second event ends the local session. It uses the host's Python and Docker and does not import this library. It uses two high ports (defaults 18556 and 18656, LIVE_STS_PORT and LIVE_DEMO_PORT) and never binds 3000, 8081 or 18481-18495. No secret appears on a docker command line, and sign-in requests are logged by fixed labels, not URLs. The nightly workflow (06:35 UTC) runs the unit suite and this script once every 24 hours.
Layout
src/wsfed_caep/librarytests/pytest, fixtures, stub identity providerdemo/app.pystdlib HTTP demoscripts/configure_iya_sts.pyregisters the demo with iya-sts and writes the listener CA trust chain to the fileidpCanamesscripts/trust_chain.pyholdssave_trust_chain()and refreshes that filescripts/live_iya_sts.pylive sign-in against iya-sts.github/workflows/nightly.ymlunit suite and live script, once every 24 hoursDockerfile,docker-compose.yml(network_mode: host,PORT=3000)
The iya-sts trust chain (idpCa)
idpCa in demo/wsfed.json names the iya-sts TLS listener CA trust chain the library trusts for the IdP metadata URL and the Shared Signals transmitter. The default is file:generated/idp-ca.pem, relative to demo/. The value may also be a plain path, env:<NAME>, or the PEM itself. The library loads every certificate in it as a trust anchor (ssl load_verify_locations(cadata=...)), so the whole chain works and OpenSSL ends the path at the self-signed Root. The presented leaf must still verify to one of those anchors (a forged leaf followed by the real CA certificates is refused), and the host name is checked against the leaf's subject alternative names (a DNS name, or an IP literal such as 127.0.0.1).
scripts/configure_iya_sts.py, the install script, writes that file on every run (scripts/trust_chain.py, save_trust_chain()). 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. A password iya-sts generates for the test user is never printed: it goes to demo/generated/<user>.password, mode 0600.
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, and rebuild the image after running the install script against a different iya-sts. .dockerignore admits only that file from demo/generated: the relying party's key, certificate and the IdP metadata stay out of the image (compose still mounts demo/generated read-only for them). A build with no chain file yet (a fresh clone, CI) still succeeds; the image then has no idpCa file.
The mechanism is COPY demo ./demo plus .dockerignore's demo/generated/* and !demo/generated/idp-ca.pem. Do not replace it with a COPY that names the file or globs for it: when the file is absent, both BuildKit and the classic builder fail. A directory COPY of demo/ (which always exists) succeeds in both. python scripts/trust_chain.py --config demo/wsfed.json --idp <iya-sts URL> refreshes the file by hand.
Demo
PORT defaults to 3000. A loopback realm and reply URL follow that port. Any other host stays as written. PUBLIC_URL or --public-url replaces the origin and keeps the path. The default http://localhost:3000 is not an explicit public URL. GET /healthz returns 200 and the body ok even when generated keys are absent. GET /login redirects to the identity provider with wa=wsignin1.0, wtrealm, wreply, and wctx. The reply URL accepts the sign-in POST and wsignoutcleanup1.0. POST /logout signs out, including at the identity provider. GET /live-status is JSON: signedIn, subject, the receiver state, and the Shared Signals events this process has accepted (type and jti only). No token is in that body.
Demo JSON uses file: paths under demo/generated/. Those keys are not in the tree. "signals" is omitted, so the receiver is on when a transmitter is configured.
PORT=40123 docker compose up --build
Metadata
Release files for wsfed-caep 0.1.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 |
|---|---|---|---|---|
| wsfed_caep-0.1.0.tar.gz | 50.9 kB | With provenance | Details for wsfed_caep-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| wsfed_caep-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for wsfed_caep-0.1.0-py3-none-any.whl |
Total release size: 92.4 kB
Release files / wsfed_caep-0.1.0.tar.gz
| Download URL | wsfed_caep-0.1.0.tar.gz |
|---|---|
| Size | 50.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bdc5447a79f7520f65ac11b205b50d0ade00352b7cb77ab1cb23efd892cc7886
|
|
BLAKE2b-256 checksum How to use checksums |
ef31bfb99c7ed6d8b83397b0a62490011bcceb4424c709d35ee3f9eb9b3ae47b
|
| 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 2, 2026.
Transparency logRelease files / wsfed_caep-0.1.0-py3-none-any.whl
| Download URL | wsfed_caep-0.1.0-py3-none-any.whl |
|---|---|
| Size | 41.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
81088385308d625689ad705ffe3321eb2ac17c0d16001e67ce40c12d9578854c
|
|
BLAKE2b-256 checksum How to use checksums |
a7d3b1ab41acc806ffe785602189da2aee37471271a5fc7a943cab881de1dad5
|
| 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 2, 2026.
Transparency logSecurity for wsfed-caep 0.1.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 wsfed_caep-0.1.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 2, 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.