Skip to content

feat: harden agent auth — JTI nonce registry, JWKS endpoint, external IdP federation #377

Description

@manojbajaj95

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:
    1. Generates a new Ed25519 key pair.
    2. Registers the new DID with the daemon.
    3. Marks the old key as rotating (still valid for verification for a configurable overlap window, default 24 h).
    4. 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

  • A replayed PoP JWT is rejected on a second replica when Redis nonce store is active.
  • GET /.well-known/jwks.json returns valid JWK Set; passes validation with a standard JWKS consumer.
  • GET /.well-known/openid-configuration returns at minimum issuer and jwks_uri.
  • authsome identity rotate produces a new key; old key still validates during the overlap window; JWKS endpoint serves both.
  • When AUTHSOME_EXTERNAL_IDP_ISSUER is set, a valid Bearer JWT from that IdP is accepted by protected daemon routes.
  • An expired, tampered, or wrong-audience IdP token is rejected with 401.
  • uv run pytest passes including new tests for nonce replay, JWKS shape, and IdP JWT validation.
  • uv run ruff check and uv run ty check pass.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions