Skip to content

feat(identity): the Principal adapter, read from identity's contract - #2

Merged
ExtraToast merged 3 commits into
mainfrom
feat/identity-adapter
Oct 20, 2026
Merged

ExtraToast merged 3 commits into
mainfrom
feat/identity-adapter

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

python-commons gets its first package, jorisjonkers_commons.identity: the Python adapter for identity's Principal contract (JorisJonkers-dev/identity#110, spec JorisJonkers-dev/identity#105, Epic JorisJonkers-dev/identity#114). The repository itself was bootstrapped from repo-template. It has the workspace-libs topic and the Main ruleset requiring Pipeline Complete, and release-please is set up as a python release with the version in the release PR title.

PrincipalReader.read is the one entry point. It takes a request's headers by name and returns a Principal, or raises PrincipalRefused with the contract's reason:

  • The Gate's signed header. X-Identity-Principal, typ identity-principal+jwt, is verified against the Issuer's JWKS. The plain X-Auth-Request-* headers are never read.
  • A JWT access token. It is verified the same way, with typ JWT and a client_id required (not-an-access-token otherwise).
  • An opaque token. It is introspected with the Client's secret, and each answer is kept for 30 seconds.

Unknown key ids refetch the JWKS at most once a minute. IdentityUnavailable means identity could not be asked, which is not a refusal.

The policy helpers are plain ASGI, so they wrap a Starlette or FastAPI app or one of its routes:

  • principal_middleware answers 401 to a refused request and 503 when identity is down. A refused WebSocket is closed with 1008.
  • require_role, refuse_tools, require_user and require_machine each answer 403 for the case they refuse.
  • require_recent_two_factor(minutes) sends a stale session 303 to /.auth/step-up?minutes=…&rd=…. It answers 403 to a Personal Token, a tool or a machine.

The Principal dataclass is generated from the pinned schema by scripts/generate_principal.py, and a golden test fails when the two disagree. Signatures use cryptography directly, with no JWT library.

Checks

  • Conformance. Every vector of the pinned contract reads as it says: 15 signed-header, 11 access-token and 5 introspection. An access token sent as the signed header, and a signed header sent as a bearer token, are both refused as wrong-typ.
  • Red checks. I removed each check in turn and watched a test fail: every policy helper's condition, the 401 and 403 branches, the step-up branch and the two-factor window, plus the reader's typ, issuer, audience, expiry, major, client_id and active checks.
  • Revocation. A token revoked at t₀ still reads at t₀+29s and is refused as inactive at t₀+30s. Five reads ask identity once.
  • Gates. task check runs ruff, ruff format, mypy --strict, actionlint, pytest with a 95% branch-coverage gate (100% now), uv build and gitleaks. The vendored vectors are allowlisted in .gitleaks.toml, because they hold tokens signed by a test key.

Contract

contract/ is pinned at contract/principal/v1.1.0, released by JorisJonkers-dev/identity#132.

Closes JorisJonkers-dev/identity#110

@ExtraToast ExtraToast self-assigned this Oct 20, 2026
@ExtraToast ExtraToast added type: feature New user-facing or operator-facing capability. area: shared-libs Shared libraries, commons packages, and reusable SDK code. area: auth Authentication, authorization, sessions, or identity. labels Oct 20, 2026
@ExtraToast
ExtraToast marked this pull request as ready for review October 20, 2026 20:05
@ExtraToast
ExtraToast merged commit 8d3c309 into main Oct 20, 2026
2 checks passed
@ExtraToast
ExtraToast deleted the feat/identity-adapter branch October 20, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Authentication, authorization, sessions, or identity. area: shared-libs Shared libraries, commons packages, and reusable SDK code. type: feature New user-facing or operator-facing capability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

python-commons, with the Python adapter

1 participant