Skip to content

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

@manojbajaj95

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions