Summary
Strengthen the current PoP-JWT agent authentication scheme with a durable JTI nonce registry, a standards-compliant JWKS discovery endpoint, and a federation layer that lets operators plug in an external Identity Provider (OIDC/SAML) in place of — or alongside — the built-in did:key identity model.
Motivation
Scope of work
1. Durable JTI nonce registry
- Move the JTI replay cache (
src/authsome/identity/proof.py) from in-process dict to a backend-agnostic NonceStore interface.
- Provide two implementations:
RedisNonceStore — keys with TTL equal to the PoP token window (production).
InMemoryNonceStore — existing behaviour, for local dev / tests.
AUTHSOME_NONCE_BACKEND=redis|memory selects the implementation; default memory.
- Guarantee that a replayed JTI is rejected across any replica.
2. JWKS discovery endpoint
- Add
GET /.well-known/jwks.json to the daemon HTTP server.
- Return all active Ed25519 public keys in JWK format (
OKP key type, crv: Ed25519) per RFC 8037.
- Include standard fields:
kid (derived from the DID fragment), use: sig, alg: EdDSA.
- Add
GET /.well-known/openid-configuration stub that at minimum advertises jwks_uri, issuer, and id_token_signing_alg_values_supported: ["EdDSA"] — enough for relying parties to auto-discover keys.
- Keys must be rotatable: the endpoint should serve all non-expired public keys so tokens signed with a recently-rotated key still validate during the overlap window.
3. Key rotation
- Add
authsome identity rotate [<handle>] CLI command that:
- Generates a new Ed25519 key pair.
- Registers the new DID with the daemon.
- Marks the old key as
rotating (still valid for verification for a configurable overlap window, default 24 h).
- Updates
active_identity in GlobalConfig.
- The JWKS endpoint serves both the active and
rotating keys during the overlap window.
4. External IdP federation (OIDC)
- Introduce an
ExternalIdpConfig model (src/authsome/identity/external_idp.py) with fields:
issuer — OIDC issuer URL (used for discovery).
client_id, client_secret — for server-to-server introspection or token exchange (optional).
audience — expected aud claim.
claim_mapping — map IdP claims to authsome PrincipalId / VaultId (e.g. sub → principal_id, email → handle).
jwks_uri — override if the IdP does not publish a standard discovery document.
AUTHSOME_EXTERNAL_IDP_ISSUER and friends configure the IdP; when set, the daemon accepts Bearer JWTs issued by that IdP in addition to native PoP JWTs.
- Validation: fetch IdP JWKS (with caching + refresh), verify signature,
exp, iat, aud, iss. Reject on any failure.
- Map the verified identity to an existing Principal (by email/sub claim) or auto-provision a new Principal+Vault on first login (configurable via
AUTHSOME_IDP_AUTO_PROVISION=true).
- Keep the
auth/ module as a leaf — IdP federation logic lives in server/ (e.g. src/authsome/server/idp_verifier.py).
5. SAML (stretch goal)
- Define the interface so a SAML assertion verifier could plug in alongside OIDC without changing the
server/ routing layer.
- Defer implementation unless there is confirmed operator demand.
6. Audit logging
- Every authentication event (PoP success/failure, IdP success/failure, key rotation, JTI replay attempt) must emit a structured audit log entry via the existing audit layer.
Acceptance criteria
Implementation notes
auth/ remains a leaf module — no imports from vault/, server/, or audit/. IdP verification belongs in server/.
- JWKS key cache for external IdPs must refresh on a TTL (e.g. 1 h) and re-fetch immediately on a
kid miss (the standard "try-refresh-on-unknown-kid" pattern).
- Do not add
python-jose or PyJWT if cryptography + manual claim validation suffices — prefer fewer deps. If a library is added, pin a minimum version with a known-good security record.
- Feature-flag IdP federation behind
AUTHSOME_EXTERNAL_IDP_ISSUER so the default path is unchanged for existing deployments.
References
Summary
Strengthen the current PoP-JWT agent authentication scheme with a durable JTI nonce registry, a standards-compliant JWKS discovery endpoint, and a federation layer that lets operators plug in an external Identity Provider (OIDC/SAML) in place of — or alongside — the built-in
did:keyidentity model.Motivation
Scope of work
1. Durable JTI nonce registry
src/authsome/identity/proof.py) from in-process dict to a backend-agnosticNonceStoreinterface.RedisNonceStore— keys with TTL equal to the PoP token window (production).InMemoryNonceStore— existing behaviour, for local dev / tests.AUTHSOME_NONCE_BACKEND=redis|memoryselects the implementation; defaultmemory.2. JWKS discovery endpoint
GET /.well-known/jwks.jsonto the daemon HTTP server.OKPkey type,crv: Ed25519) per RFC 8037.kid(derived from the DID fragment),use: sig,alg: EdDSA.GET /.well-known/openid-configurationstub that at minimum advertisesjwks_uri,issuer, andid_token_signing_alg_values_supported: ["EdDSA"]— enough for relying parties to auto-discover keys.3. Key rotation
authsome identity rotate [<handle>]CLI command that:rotating(still valid for verification for a configurable overlap window, default 24 h).active_identityinGlobalConfig.rotatingkeys during the overlap window.4. External IdP federation (OIDC)
ExternalIdpConfigmodel (src/authsome/identity/external_idp.py) with fields:issuer— OIDC issuer URL (used for discovery).client_id,client_secret— for server-to-server introspection or token exchange (optional).audience— expectedaudclaim.claim_mapping— map IdP claims to authsomePrincipalId/VaultId(e.g.sub → principal_id,email → handle).jwks_uri— override if the IdP does not publish a standard discovery document.AUTHSOME_EXTERNAL_IDP_ISSUERand friends configure the IdP; when set, the daemon accepts Bearer JWTs issued by that IdP in addition to native PoP JWTs.exp,iat,aud,iss. Reject on any failure.AUTHSOME_IDP_AUTO_PROVISION=true).auth/module as a leaf — IdP federation logic lives inserver/(e.g.src/authsome/server/idp_verifier.py).5. SAML (stretch goal)
server/routing layer.6. Audit logging
Acceptance criteria
GET /.well-known/jwks.jsonreturns valid JWK Set; passes validation with a standard JWKS consumer.GET /.well-known/openid-configurationreturns at minimumissuerandjwks_uri.authsome identity rotateproduces a new key; old key still validates during the overlap window; JWKS endpoint serves both.AUTHSOME_EXTERNAL_IDP_ISSUERis set, a valid Bearer JWT from that IdP is accepted by protected daemon routes.uv run pytestpasses including new tests for nonce replay, JWKS shape, and IdP JWT validation.uv run ruff checkanduv run ty checkpass.Implementation notes
auth/remains a leaf module — no imports fromvault/,server/, oraudit/. IdP verification belongs inserver/.kidmiss (the standard "try-refresh-on-unknown-kid" pattern).python-joseorPyJWTifcryptography+ manual claim validation suffices — prefer fewer deps. If a library is added, pin a minimum version with a known-good security record.AUTHSOME_EXTERNAL_IDP_ISSUERso the default path is unchanged for existing deployments.References
src/authsome/identity/proof.pysrc/authsome/identity/local.pysrc/authsome/server/settings.pysrc/authsome/server/routes/_deps.pysrc/authsome/audit/