1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//! Cached Issuer JWKS lookup (attestation trust needs `jwks.get(att.claims.iss, att.header.kid)`).
//!
//! ## Why this is a small hand-rolled cache instead of `authkestra_resource::jwt::JwksCache`
//!
//! The obvious move is to reuse `authkestra_resource::jwt::JwksCache` — it already exists, is
//! already depended on by `authkestra-oidc`, and this crate should not reimplement JWKS
//! fetching/rotation from scratch. Read against the real, current source
//! (`crates/authkestra-resource/src/jwt.rs`) rather than assumed, it turns out not to be a
//! drop-in fit here, for two independent reasons:
//!
//! 1. **It is a network-fetching cache with no seed/construct-from-memory path.**
//! `JwksCache::new(jwks_uri, refresh_interval)` only ever populates itself by calling
//! `Jwks::fetch(jwks_uri)` — a live `reqwest::get` — inside `refresh()`/`get_jwks()`. There is
//! no constructor that accepts an in-memory key set. That is exactly right for the
//! OAuth/OIDC resource-server case it was built for (a real Issuer is always reachable at a
//! real URL), but this crate's test suite has no live Issuer (`authkestra-op`'s enrolment/
//! attestation-minting side is tracked separately in authkestra#136) and mints test
//! attestations directly — exercising the real cache in every test would mean standing up a
//! mock HTTP server (`wiremock`, already a dev-dependency of `authkestra-resource` for exactly
//! this reason) for each one.
//! 2. **It stores `authkestra_engine::token::jwk::Jwk`, which is hard-coded RSA-only** —
//! `to_decoding_key()` explicitly errors on any non-RSA `kty`. That happens not to bite the
//! attestation side specifically (the attestation is always RS256 per its wire format), but
//! consuming it would still mean converting its `Jwk` type at the boundary for no benefit,
//! and this crate needs `jsonwebtoken::jwk::Jwk` directly anyway for the *signature* side
//! (the device key, which is EC — see `signature.rs` — and which that wrapper cannot
//! represent at all).
//!
//! So: a minimal in-memory `(issuer, kid) -> Jwk` cache, populated via [`IssuerJwks::insert`].
//! A production integration wraps this with a periodic refresh task against each trusted
//! issuer's published JWKS endpoint — `authkestra_resource::jwt::Jwks::fetch` is a perfectly
//! reasonable way to do the HTTP part of that — and calls `insert` on every refresh. That
//! refresh loop is deliberately not implemented in this crate: it is integration plumbing, not
//! part of the verification algorithm, and belongs next to whatever task-spawning convention the
//! embedding application already uses (the framework integration in `axum_integration.rs` takes
//! an `Arc<IssuerJwks>` for exactly this reason — the caller owns the refresh lifecycle).
use HashMap;
use Jwk;
use RwLock;
/// An in-memory cache of Issuer public keys, keyed by `(issuer, kid)`.
///
/// This is the "one cached public JWKS" that makes verification self-contained — no per-request
/// network call. It performs no network I/O itself; callers populate it via
/// [`IssuerJwks::insert`], however they choose to fetch and refresh keys.