Skip to main content

oauth_resource_server/
testing.rs

1//! **Test-only** fixtures for testing code that sits behind this crate: a fake
2//! authorization server ([`TestAuthority`]), a fluent token builder
3//! ([`TokenBuilder`]), and the lower-level throwaway keys, JWK builders, minting
4//! functions and fake HTTP server they are built from.
5//!
6//! Compiled for this crate's own tests and, for consumers' tests, behind the
7//! `testing` feature. **Never enable `testing` in a production build**: the
8//! private keys here are public knowledge, and anything that trusts them trusts
9//! everyone. Enable it from `[dev-dependencies]` only.
10//!
11//! # Semver
12//!
13//! This module follows semver like the rest of the crate: removing or renaming
14//! an item, or changing what an existing one does, is a breaking change and
15//! ships in a new `0.x` minor. Test suites are consumers too, and an exemption
16//! would make every upgrade a coin toss for them. The throwaway keys, their
17//! `kid`s and the keys [`TestAuthority`] serves are stable in the same sense.
18//! What is *not* promised is the wording of a panic message. Adding an item, or
19//! a key to the served JWK Set, is additive.
20//!
21//! # Start with [`TestAuthority`]
22//!
23//! [`TestAuthority::start`] runs a loopback fake authorization server that
24//! serves discovery (both OpenID Connect and RFC 8414) and a JWKS, with its
25//! own issuer and `jwks_uri`. [`TestAuthority::config`] resolves a
26//! [`ResolvedOAuthConfig`] consistent with it, and [`TestAuthority::token`]
27//! builds tokens that config accepts, so the common test is three lines and each
28//! thing a test wants *wrong* is one builder call. Nothing in it is specific to
29//! a provider or an application: the defaults are neutral
30//! (`https://api.example.test/`, scope `api:read`).
31//!
32//! ```
33//! use oauth_resource_server::testing::TestAuthority;
34//! use oauth_resource_server::{Algorithm, OAuthValidator, TokenRejection};
35//!
36//! # #[tokio::main(flavor = "current_thread")]
37//! # async fn main() {
38//! let authority = TestAuthority::start().await;
39//! let validator = OAuthValidator::new(&authority.config(|_| {})).unwrap();
40//!
41//! // The defaults are a token that config accepts.
42//! let token = validator.validate(&authority.token().sign()).await.unwrap();
43//! assert!(token.has_scope("api:read"));
44//!
45//! // Each knob makes exactly one thing wrong (or different).
46//! let expired = authority.token().expired().sign();
47//! assert!(matches!(
48//!     validator.validate(&expired).await,
49//!     Err(TokenRejection::Invalid(_))
50//! ));
51//! let no_scope = authority.token().scopes(["other:scope"]).sign();
52//! assert!(matches!(
53//!     validator.validate(&no_scope).await,
54//!     Err(TokenRejection::InsufficientScope)
55//! ));
56//! let es256 = authority.token().alg(Algorithm::ES256).sign();
57//! assert!(validator.validate(&es256).await.is_ok());
58//! # }
59//! ```
60//!
61//! Handler tests that do not need a real token can skip validation altogether
62//! and build the verified value directly with
63//! [`AuthorizedToken::new`](crate::AuthorizedToken::new) and its `with_*`
64//! builders, for example
65//! [`with_claims`](crate::AuthorizedToken::with_claims).
66//!
67//! # Testing an axum handler end to end
68//!
69//! With the `axum` feature, run a real request through
70//! [`AuthLayer`](crate::axum::AuthLayer) and the
71//! [`AuthorizedToken`](crate::AuthorizedToken) extractor. This needs `tower`
72//! with its `util` feature (for `ServiceExt::oneshot`) as a dev-dependency.
73//! The body is the same as the README's "Testing your integration" example, so
74//! that example is compiled and run here (`#` lines only gate it on the
75//! `axum` feature):
76//!
77//! ```
78//! # #[cfg(feature = "axum")]
79//! use std::sync::Arc;
80//!
81//! # #[cfg(feature = "axum")]
82//! use axum::{Router, body::Body, http::{Request, StatusCode}, routing::get};
83//! # #[cfg(feature = "axum")]
84//! use oauth_resource_server::axum::AuthLayer;
85//! # #[cfg(feature = "axum")]
86//! use oauth_resource_server::testing::TestAuthority;
87//! # #[cfg(feature = "axum")]
88//! use oauth_resource_server::{AuthorizedToken, OAuthValidator};
89//! # #[cfg(feature = "axum")]
90//! use tower::ServiceExt; // for `oneshot`
91//!
92//! # #[cfg(feature = "axum")]
93//! #[tokio::main(flavor = "current_thread")]
94//! async fn main() {
95//!     let authority = TestAuthority::start().await;
96//!     // Adjust anything before the config is resolved; it panics with the
97//!     // `ConfigError` text if the result is invalid.
98//!     let config = authority.config(|c| c.require_at_jwt = true);
99//!     let validator = Arc::new(OAuthValidator::new(&config).unwrap());
100//!
101//!     let app = Router::new()
102//!         .route(
103//!             "/whoami",
104//!             get(|token: AuthorizedToken| async move { token.subject.unwrap_or_default() }),
105//!         )
106//!         .route_layer(AuthLayer::builder().oauth(validator).build().unwrap());
107//!     let request = |bearer: String| {
108//!         Request::builder()
109//!             .uri("/whoami")
110//!             .header("authorization", format!("Bearer {bearer}"))
111//!             .body(Body::empty())
112//!             .unwrap()
113//!     };
114//!
115//!     let ok = app.clone().oneshot(request(authority.token().subject("ada").sign())).await.unwrap();
116//!     assert_eq!(ok.status(), StatusCode::OK);
117//!
118//!     let expired = app.clone().oneshot(request(authority.token().expired().sign())).await.unwrap();
119//!     assert_eq!(expired.status(), StatusCode::UNAUTHORIZED);
120//!
121//!     let no_scope = app.oneshot(request(authority.token().scopes(["other:scope"]).sign())).await.unwrap();
122//!     assert_eq!(no_scope.status(), StatusCode::FORBIDDEN);
123//! }
124//! # #[cfg(not(feature = "axum"))]
125//! # fn main() {}
126//! ```
127//!
128//! # The lower-level building blocks
129//!
130//! Everything below [`TestAuthority`] stays available: the key constants
131//! ([`KEY_A_PEM`], [`KEY_B_PEM`], [`EC_PEM`], [`ED_PEM`]), the JWK builders
132//! ([`jwk_rsa_a`], [`jwk_ec`], [`jwk_ed`], [`jwks_of`], [`jwks_body`],
133//! [`jwks_body_all`]), [`mint`], [`mint_with`] and [`valid_token`], the fake
134//! HTTP server ([`FakeJwksServer`], [`spawn_jwks_server`],
135//! [`spawn_http_server`]) and [`resolved_config`].
136//!
137//! Those fixtures model a plausible Authentik deployment (per-application issuer
138//! with a trailing slash, client-id audience, `mcp:read`/`mcp:write` scopes)
139//! because that is the production shape this crate's own regression tests were
140//! written against. That shape is an implementation detail, not a recommendation
141//! and not something the crate requires. Reach for them when a test needs to
142//! control something [`TestAuthority`] does not, such as a hand-built JWK Set
143//! or a misbehaving server. [`mint`] and [`mint_with`] take arbitrary claims, so
144//! a test can use its own issuer, audience and scopes.
145//!
146//! ```
147//! use oauth_resource_server::OAuthValidator;
148//! use oauth_resource_server::testing;
149//!
150//! # #[tokio::main(flavor = "current_thread")]
151//! # async fn main() {
152//! let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
153//! let validator = OAuthValidator::new(&testing::resolved_config(&jwks.url)).unwrap();
154//!
155//! let token = validator.validate(&testing::valid_token()).await.unwrap();
156//! assert!(token.has_scope("mcp:read"));
157//!
158//! // A token for this test's own claims, signed with the published test key.
159//! // Any `Serialize` value works as claims: a `json!` literal, or a struct.
160//! let unscoped = testing::mint(
161//!     testing::KEY_A_PEM,
162//!     testing::KID_A,
163//!     &serde_json::json!({
164//!         "iss": testing::ISSUER,
165//!         "aud": testing::AUDIENCE,
166//!         "exp": testing::now() + 60,
167//!     }),
168//! );
169//! assert!(validator.validate(&unscoped).await.is_err()); // lacks the required scope
170//! # }
171//! ```
172
173use std::collections::HashMap;
174use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
175use std::sync::{Arc, Mutex};
176use std::time::Duration;
177
178use jsonwebtoken::{EncodingKey, Header, encode};
179use serde::Serialize;
180use tokio::io::{AsyncReadExt, AsyncWriteExt};
181use tokio::net::TcpListener;
182
183use crate::algorithms::{Algorithm, DEFAULT_ALGORITHMS, parse_algorithm};
184use crate::config::{
185    DEFAULT_LEEWAY_SECS, DEFAULT_PRINCIPAL_CLAIMS, DEFAULT_SCOPE_CLAIMS, KeyNaming, KeyNamingBuf,
186    OAuthConfig, ResolvedOAuthConfig,
187};
188
189/// `kid` of the throwaway 2048-bit RSA keypair [`KEY_A_PEM`], generated for this
190/// test suite and used nowhere else.
191pub const KID_A: &str = "test-key-a";
192/// Modulus of [`KEY_A_PEM`]'s public half, base64url, as a JWK `n`.
193pub const N_A: &str = "zXtrd9E8iuVecx_7KN0nxRV0m0DgZayGgW5D4bPJMwUcFX6SIsyYpSCAGjT1Fia85xH-YrMxk9XSjuMpYB8GphQ5NitAaVx8CQeoVQw8WEi1YSG53OfuSftmkX79D48nVP6VxKq3JW_RIaTM8xsisVV2zzFeQVN_NsFNCAsClYoXLUj8Wfc9WsFz8DszbQep6I4gceD6WNCs72AQMXR5vIOfGxK5eP5JWOjK7FN95njVNbXY6p5QUQii_3HkFSDQv9drzpzeKXdDziFdSG5qZfMwGuqjfCMDNfwYKxC4AbAGbtSTCHFEWe0CuWX95xgqvyJCsVjkh8xMz-WpPoWLSQ";
194
195/// Throwaway RSA private key A (PKCS#8 PEM). Its public half is [`jwk_rsa_a`].
196pub const KEY_A_PEM: &str = "-----BEGIN PRIVATE KEY-----
197MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNe2t30TyK5V5z
198H/so3SfFFXSbQOBlrIaBbkPhs8kzBRwVfpIizJilIIAaNPUWJrznEf5iszGT1dKO
1994ylgHwamFDk2K0BpXHwJB6hVDDxYSLVhIbnc5+5J+2aRfv0PjydU/pXEqrclb9Eh
200pMzzGyKxVXbPMV5BU382wU0ICwKVihctSPxZ9z1awXPwOzNtB6nojiBx4PpY0Kzv
201YBAxdHm8g58bErl4/klY6MrsU33meNU1tdjqnlBRCKL/ceQVINC/12vOnN4pd0PO
202IV1Ibmpl8zAa6qN8IwM1/BgrELgBsAZu1JMIcURZ7QK5Zf3nGCq/IkKxWOSHzEzP
2035ak+hYtJAgMBAAECggEAPpyQWxKZGZOZi4ffroxw3VdT0CjdF24SECdKrN/s+0xf
204ydbm7Y6dJpe4IQQo+AZ2wgwUEPwcK7lYLuzeAymBC6MW6cAVIOWq789zBfM0Agyp
205o/60VTEgxU9C6iuhLZgHupjWhvYj11byiQdf4eXPVOy/RpP67fnkxgjxkXVVZL4C
206zJ5KQZRLi+DH9l5Vd5nKqyRVVFVaaD0ws5Lw7n2HBrraq/omV6FlcIkePB4Tx2gD
207WudBhUPnrhukXaoEWEvBNXnVSExU+bZMeWvQdcGVL6OE1LG9IqsjYiumF2kb0n0L
208ZTalPbtAHoNDEKIG2+rwCqsLBZQvFnLcFTlc1WAExwKBgQDr8IjWaZ8I0mQHbRhM
209BsrLDjf2qBVclBMchYafmh6NoI5E2+928NTo/uxssGTEX4ce0v6CW6dHo8vNVfN/
210cUxjQW479qvugi8EBp6rQ9ZOjSra078L145jTCaJLfxMuYDgjycvcv8wOqxJL/80
211F1Qjkn+pGsQFvjCAEikjwcG0cwKBgQDe8/J8W2tPpnRiv40T2Hgu4Th8gRfm/79k
212RBZqeiO/EiI9zwHmOj9s02fK3tBPyQgZMyQyQNMJjFuWzCjHE8gaBdLAqGrpILL2
213jR7EvXBPrGRpWcbnRCyODURcaIY1dTVImT1g9rhwzDJNQFd7XPGR60LCMUxL8b2p
214hlgFlw9OUwKBgFAqvpP792mL8ykCzIqolCdCgYlxuzBlr8i1JfT87Py6XRzQjiEf
21523f/hl234cVHoCW9E3U/pysUYJ84YTAgUxA2nzoIqoqz+T2o8ijHN/4gwTrxT6y6
216ZUsgCMf7tAptzXh/q5TXwhWlGf0ULeaJNrGPiYjv60L4SIp7oTbhEuw5AoGAdJg8
217yn4Am7HgEbg87hD5oQKVSL82Ic7DZ4sX8e0X/pdcIti8FIuHmcDg+b4WUHNAcfVF
218y6YM92RYjX8NIDcfIUTEV458ApjgHoHkglzTfEcaZ+HUXCNR7aPQiUb8UL6P8/x3
219ldrQz+Rpte6dEV2k03ul+OpRDTJJznr8U0gRcBMCgYB/aUF16/RJvW5nLGWTbAS8
220D4d9SgETq0P0zbuDUk60Fk6kQbQ+bwX+ffgsEP/P/CFTNJ+opoCo0/6uK8WlKs15
221uVx1QLn2oATcEUusHESeflBUSSaYlhHXFL7ahvAgBs3vzgWZnUVnz2A3QDCiLu6H
222EmjUKNFGC0zInLUM1Cbu9w==
223-----END PRIVATE KEY-----
224";
225
226/// A second throwaway RSA private key (public knowledge, like every key in this
227/// module) whose public half is in no fixture JWKS: it exists only to produce a
228/// signature that [`KEY_A_PEM`]'s public half must reject.
229pub const KEY_B_PEM: &str = "-----BEGIN PRIVATE KEY-----
230MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClweugXTF1SY0q
231ar8Z68ong9eCzOI3kCSipuiCDhVPad8Gn4be0RM4B7t342iuG4UjyXnCQpCoWGiN
232L4KN52hFBE7M8c/7JutAtmpJFm33cFKZ+yWfAcX5FFtC/BdOPfaPtije98QJRmlv
233lJ6n7c8uMpXhtV1ZIqwm9g7chVWlUHKAgMFGaUeKWdksQ9tTZgDKeHO1vfRZZlYT
234XdvDpNe7Dxz1o3eefTsrKsE1DDTXrDfJPUDPPpBTMmT+xrPRehuNqrNQRUJWEIAR
235bJpV5ltnhNX4zs3YQ39/XTCcQjnbu4wRpDUgTIhPuomg18t8vqi1CbhaN8+Ww3oU
236FRdXKE59AgMBAAECggEAMeZ2umDD3mTFmCLpo/KNeabhrrFiWsrMlKC9t0VpGe6r
2374xEMZ7C2YfRF9hoibePACZ2CR76FUQDIfNR0L6ceB0T8OguECr5VLTadOaKEeWy5
238mTx3v24nvMvpi3lbxMS3oNz8Yd9iB07It/wcZT6c0/ILmBbi4s4i2FnT8IQ9W9Ym
239iuMwqujeyrEUG/O3HrUJLHNe6PwJj6s8mbAKxfmCqnDLCyWlQejR2FL24mriCSQD
240G03gZ6VazAnDt19SjToPKH1e6XjB6FySUX3gDhA9yXSPphCdaa9Ov9w/4UktlEtz
241RRobV9e+e5e2qUrv77CZu3PMlH/gZARM/ncSCKlYGwKBgQDqVHUYvBArQWLO2po6
2429SvjcvB631+cUO94k+nV4vlbXbzjMznXPPShijf5cirzRhazSRqxp5Yt6959vVDI
243Pe/vjP0lM0dLZwiOnsEaq+ArEoAnidD09bUn81qMnsH3eUpXtIthB5ltkNX3tXb2
244Xs04OxDm4IrSoMg7/w2HXb4YbwKBgQC1FhMz8PvXDAb7uJ5ydjd4Gw1CEqw93YSf
245S1koX2x86qhELfUglAHb+h5RHxE7zqi5fqzrsHl3Ow3392O3clcqrbME4u//fvmV
246XCr7eHraeIByX/ZpnBiiuYjrvN6MKDUy00yBDdGMGGA0JT2+aH/06qg/G5xG8SiV
2470ajx8wAF0wKBgQDnbTQckpfRcIk6TBFoSvzmbHzujS9rPU/UoRifAcRNpP1I0i28
2480lm0NMLlXAjpLH584KU5cY7TmZCqVE+1A960kmTs2YD/CiocWNPUGI2TXHkvE2BI
249nWYlp6T1HlHorGRszEWfNZck65c2RoTP+37omwUtT/Qq41n+Tv44g6+bhwKBgGyQ
250EW8gWDsycLVUl1lT2ildPnOQMkbcmPfO+mKj4qx5GevWCZFAamTw7GAB2hka6jha
25141xhblC2zMcOP2/pUqy5egvB6dQo0YRjvzkHn89+UrM/KMFj3bkgth9uGZW5PTt9
252Re5Q1IHC01ovwXZ3u86fJ8K90NEPHx/ClCCJaEgVAoGAHlopDQ/w7JN5sCBYDZeE
253eAfND1Q/hnbfjdUgg13/Qmhqwm86RYJ3E9mxjcCNKZ3hNX3Xcs5NW5oC9tj9Nb9G
254B5bK2earcA3sKw66Uvzd5AtypET7/RPOSgpXOD34f1RN38fWqc+L0pdHZ41D5eif
25513kE7LEf//HMi5ix93dRdZw=
256-----END PRIVATE KEY-----
257";
258/// `kid` of [`KEY_B_PEM`]'s public half as [`TestAuthority`] publishes it.
259pub const KID_B: &str = "test-key-b";
260/// Modulus of [`KEY_B_PEM`]'s public half, base64url, as a JWK `n` (the public
261/// half of the existing throwaway key, not new key material).
262pub const N_B: &str = "pcHroF0xdUmNKmq_GevKJ4PXgsziN5Akoqbogg4VT2nfBp-G3tETOAe7d-NorhuFI8l5wkKQqFhojS-CjedoRQROzPHP-ybrQLZqSRZt93BSmfslnwHF-RRbQvwXTj32j7Yo3vfECUZpb5Sep-3PLjKV4bVdWSKsJvYO3IVVpVBygIDBRmlHilnZLEPbU2YAynhztb30WWZWE13bw6TXuw8c9aN3nn07KyrBNQw016w3yT1Azz6QUzJk_saz0XobjaqzUEVCVhCAEWyaVeZbZ4TV-M7N2EN_f10wnEI527uMEaQ1IEyIT7qJoNfLfL6otQm4WjfPlsN6FBUXVyhOfQ";
263
264/// `kid` of the throwaway P-256 keypair [`EC_PEM`] (generated with
265/// `openssl genpkey`, used nowhere else) — so ES256, Kanidm's default, is
266/// exercised end to end, not just RS256.
267pub const KID_EC: &str = "test-key-ec";
268/// Throwaway P-256 private key (PKCS#8 PEM). Its public half is [`jwk_ec`].
269pub const EC_PEM: &str = "-----BEGIN PRIVATE KEY-----
270MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgt+Eh+ZhHxw1rLcOh
271VFTMghCKj2Vjq7F3zWwemIamL62hRANCAATCkjHQ5M6RrM1TPQ6wuvqltcwRa4AL
272s/Jd92N5PXaKwn94PezTTY6vFt/ivjcfSSG5wWncUlc92lsipOXRqgLZ
273-----END PRIVATE KEY-----
274";
275/// [`EC_PEM`]'s public `x` coordinate, base64url.
276pub const EC_X: &str = "wpIx0OTOkazNUz0OsLr6pbXMEWuAC7PyXfdjeT12isI";
277/// [`EC_PEM`]'s public `y` coordinate, base64url.
278pub const EC_Y: &str = "f3g97NNNjq8W3-K-Nx9JIbnBadxSVz3aWyKk5dGqAtk";
279
280/// `kid` of the throwaway Ed25519 keypair [`ED_PEM`] (generated with
281/// `openssl genpkey`, used nowhere else) — so EdDSA is exercised end to end.
282pub const KID_ED: &str = "test-key-ed";
283/// Throwaway Ed25519 private key (PKCS#8 PEM). Its public half is [`jwk_ed`].
284pub const ED_PEM: &str = "-----BEGIN PRIVATE KEY-----
285MC4CAQAwBQYDK2VwBCIEICpZSYX0J1AafpNnoaSXF7Lm/Nmt73HqecXyoFLjchf8
286-----END PRIVATE KEY-----
287";
288/// [`ED_PEM`]'s public key, base64url, as a JWK `x`.
289pub const ED_X: &str = "C4HUUU7zy0ZEyY__PV16YbPgh4b4clhBg0oVMg0_EaQ";
290
291/// The fixture issuer: an Authentik-style per-application issuer
292/// (`<host>/application/o/<slug>/`), trailing slash included. The host and the
293/// application slug are placeholders.
294pub const ISSUER: &str = "https://authentik.example.test/application/o/example-app/";
295/// The fixture audience: an OAuth client_id, the way Authentik stamps `aud`.
296pub const AUDIENCE: &str = "test-client-id";
297/// The fixture protected resource.
298pub const RESOURCE: &str = "https://kb.example.test/mcp";
299
300/// The JWK for [`KEY_A_PEM`]'s public half, as an AS would serve it (`alg`
301/// RS256, `use` sig).
302pub fn jwk_rsa_a() -> serde_json::Value {
303    serde_json::json!({
304        "kty": "RSA", "use": "sig", "alg": "RS256", "kid": KID_A, "n": N_A, "e": "AQAB",
305    })
306}
307
308/// The same RSA key with no `alg`, so it may verify any RS*/PS* algorithm.
309pub fn jwk_rsa_a_any_alg(kid: &str) -> serde_json::Value {
310    serde_json::json!({"kty": "RSA", "use": "sig", "kid": kid, "n": N_A, "e": "AQAB"})
311}
312
313/// The JWK for [`EC_PEM`]'s public half (`alg` ES256).
314pub fn jwk_ec() -> serde_json::Value {
315    serde_json::json!({
316        "kty": "EC", "crv": "P-256", "use": "sig", "alg": "ES256", "kid": KID_EC,
317        "x": EC_X, "y": EC_Y,
318    })
319}
320
321/// The JWK for [`ED_PEM`]'s public half (`alg` EdDSA).
322pub fn jwk_ed() -> serde_json::Value {
323    serde_json::json!({
324        "kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA", "kid": KID_ED,
325        "x": ED_X,
326    })
327}
328
329/// A JWK Set document holding `keys`.
330pub fn jwks_of(keys: &[serde_json::Value]) -> String {
331    serde_json::json!({ "keys": keys }).to_string()
332}
333
334/// A JWK Set carrying only [`KEY_A_PEM`]'s public half — the shape Authentik
335/// serves.
336pub fn jwks_body() -> String {
337    jwks_of(&[jwk_rsa_a()])
338}
339
340/// Every test key: RSA A (RS256-labelled), a PS256-capable copy of it (`kid`
341/// `test-key-a-pss`), the P-256 key and the Ed25519 key.
342pub fn jwks_body_all() -> String {
343    jwks_of(&[
344        jwk_rsa_a(),
345        jwk_rsa_a_any_alg("test-key-a-pss"),
346        jwk_ec(),
347        jwk_ed(),
348    ])
349}
350
351/// A resolved config matching the fixtures: [`ISSUER`], [`AUDIENCE`],
352/// [`RESOURCE`], `mcp:read` required, `mcp:read mcp:write` advertised, keys at
353/// `jwks_uri` (empty means "discover from the issuer"), and every other setting
354/// at its default. Settings are named `mcp.oauth.*` in errors and logs.
355///
356/// # Panics
357///
358/// Never in practice: it would panic only if one of the crate's own default
359/// algorithm names failed to parse, which the crate's tests rule out.
360pub fn resolved_config(jwks_uri: &str) -> ResolvedOAuthConfig {
361    ResolvedOAuthConfig {
362        issuer: ISSUER.to_string(),
363        jwks_uri: (!jwks_uri.is_empty()).then(|| jwks_uri.to_string()),
364        audience: AUDIENCE.to_string(),
365        audiences: Vec::new(),
366        resource: RESOURCE.to_string(),
367        required_scopes: vec!["mcp:read".to_string()],
368        scopes_supported: vec!["mcp:read".to_string(), "mcp:write".to_string()],
369        scope_claims: DEFAULT_SCOPE_CLAIMS.iter().map(|s| s.to_string()).collect(),
370        principal_claims: DEFAULT_PRINCIPAL_CLAIMS
371            .iter()
372            .map(|s| s.to_string())
373            .collect(),
374        algorithms: DEFAULT_ALGORITHMS
375            .iter()
376            .map(|s| parse_algorithm(s).expect("every default algorithm parses"))
377            .collect(),
378        leeway_secs: DEFAULT_LEEWAY_SECS,
379        require_at_jwt: false,
380        allow_unscoped_tokens: false,
381        allow_insecure_http: false,
382        accept_static_bearer: true,
383        allowed_client_ids: Vec::new(),
384        max_token_age_secs: None,
385        required_claims: std::collections::BTreeMap::new(),
386        resource_name: None,
387        key_naming: KeyNamingBuf::Dotted("mcp.oauth".to_string()),
388    }
389}
390
391/// Seconds since the epoch, for `exp`/`nbf`/`iat`.
392///
393/// # Panics
394///
395/// If the system clock reads earlier than the Unix epoch.
396pub fn now() -> u64 {
397    std::time::SystemTime::now()
398        .duration_since(std::time::UNIX_EPOCH)
399        .expect("the clock is after 1970")
400        .as_secs()
401}
402
403/// Mint an RS256 token with full control over every field a test might want
404/// wrong. `pem` is which key signs it; `kid` is what the header *claims* signed
405/// it — letting a test say "signed by B, labelled A" for the bad-signature case.
406/// `claims` is anything that serializes to a JSON object: a
407/// `serde_json::json!` literal, or the test's own claims struct.
408///
409/// # Panics
410///
411/// If `pem` is not a valid RSA private key in PEM form, or if the claims fail
412/// to serialize.
413pub fn mint(pem: &str, kid: &str, claims: &impl Serialize) -> String {
414    let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
415    header.kid = Some(kid.to_string());
416    encode(
417        &header,
418        claims,
419        &EncodingKey::from_rsa_pem(pem.as_bytes()).expect("a valid RSA PEM"),
420    )
421    .expect("the token encodes")
422}
423
424/// Mint with an explicit algorithm, `kid` and `typ`. RS*/PS* sign with
425/// [`KEY_A_PEM`], ES256 with [`EC_PEM`], EdDSA with [`ED_PEM`]. `claims` as
426/// for [`mint`].
427///
428/// # Panics
429///
430/// If `alg` is any other algorithm (there is no test key for it), or if the
431/// claims fail to serialize.
432pub fn mint_with(
433    alg: Algorithm,
434    kid: Option<&str>,
435    typ: Option<&str>,
436    claims: &impl Serialize,
437) -> String {
438    let mut header = Header::new(alg.to_jwt());
439    header.kid = kid.map(str::to_string);
440    header.typ = typ.map(str::to_string);
441    let key = match alg {
442        Algorithm::RS256
443        | Algorithm::RS384
444        | Algorithm::RS512
445        | Algorithm::PS256
446        | Algorithm::PS384
447        | Algorithm::PS512 => EncodingKey::from_rsa_pem(KEY_A_PEM.as_bytes()),
448        Algorithm::ES256 => EncodingKey::from_ec_pem(EC_PEM.as_bytes()),
449        Algorithm::EdDSA => EncodingKey::from_ed_pem(ED_PEM.as_bytes()),
450        other => panic!("no test key for {other:?}"),
451    }
452    .expect("a valid test key");
453    encode(&header, claims, &key).expect("the token encodes")
454}
455
456/// The happy-path token for [`resolved_config`]: right key, right issuer, right
457/// audience, valid for an hour, carrying `mcp:read mcp:write`.
458pub fn valid_token() -> String {
459    mint(
460        KEY_A_PEM,
461        KID_A,
462        &serde_json::json!({
463            "iss": ISSUER,
464            "aud": AUDIENCE,
465            "azp": AUDIENCE,
466            "sub": "user-1",
467            "exp": now() + 3600,
468            "scope": "mcp:read mcp:write",
469        }),
470    )
471}
472
473/// A throwaway loopback HTTP server standing in for the authorization server,
474/// counting hits. Hand-rolled so the crate needs no HTTP-mock dependency.
475///
476/// `routes` maps a request path to `(status line, body)` and can be changed while
477/// the server runs (key rotation tests); a path with no route gets the fallback,
478/// or a 404 when there is none. Every response is `application/json`.
479///
480/// `#[non_exhaustive]`: get one from [`spawn_jwks_server`] or
481/// [`spawn_http_server`]; a field may be added without a breaking change.
482#[derive(Debug)]
483#[non_exhaustive]
484pub struct FakeJwksServer {
485    /// `http://127.0.0.1:<port>/jwks`.
486    pub url: String,
487    /// `http://127.0.0.1:<port>`, for building issuer and discovery URLs.
488    pub base: String,
489    /// Requests answered so far.
490    pub hits: Arc<AtomicUsize>,
491    /// Path → `(status line, body)`, e.g. `("200 OK", jwks_body())`.
492    pub routes: Arc<Mutex<HashMap<String, (&'static str, String)>>>,
493    /// Milliseconds to stall before answering — a slow IdP. Read when a
494    /// connection is accepted, so a change applies to the next request.
495    pub delay_ms: Arc<AtomicU64>,
496    /// While set, a request is counted in `hits` and then held until
497    /// `release` is notified (once per held request) — lets this crate's own
498    /// tests keep a fetch in flight for exactly as long as they need. Read
499    /// only by those tests, hence unused in a `testing`-feature build.
500    #[cfg_attr(not(test), allow(dead_code))]
501    pub(crate) hold: Arc<AtomicBool>,
502    /// See `hold`.
503    #[cfg_attr(not(test), allow(dead_code))]
504    pub(crate) release: Arc<tokio::sync::Notify>,
505    /// Requests answered per path, for [`TestAuthority`].
506    pub(crate) path_hits: Arc<Mutex<HashMap<String, usize>>>,
507    /// Stops the accept loop; used by [`TestAuthority`]'s `Drop`.
508    pub(crate) accept_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
509}
510
511/// Answer every request, whatever its path, with one canned response.
512///
513/// # Panics
514///
515/// As [`spawn_http_server`].
516pub async fn spawn_jwks_server(status_line: &'static str, body: String) -> FakeJwksServer {
517    spawn_http_server(HashMap::new(), Some((status_line, body))).await
518}
519
520/// Serve `routes`, falling back to `fallback` (or a 404) for any other path.
521///
522/// # Panics
523///
524/// If no loopback port can be bound. Must be called inside a tokio runtime
525/// (it spawns the accept loop).
526pub async fn spawn_http_server(
527    routes: HashMap<String, (&'static str, String)>,
528    fallback: Option<(&'static str, String)>,
529) -> FakeJwksServer {
530    let listener = TcpListener::bind("127.0.0.1:0")
531        .await
532        .expect("bind a loopback port");
533    let addr = listener.local_addr().expect("a bound address");
534    let hits = Arc::new(AtomicUsize::new(0));
535    let counter = Arc::clone(&hits);
536    let routes = Arc::new(Mutex::new(routes));
537    let shared_routes = Arc::clone(&routes);
538    let fallback = Arc::new(fallback);
539    let delay_ms = Arc::new(AtomicU64::new(0));
540    let shared_delay = Arc::clone(&delay_ms);
541    let hold = Arc::new(AtomicBool::new(false));
542    let shared_hold = Arc::clone(&hold);
543    let release = Arc::new(tokio::sync::Notify::new());
544    let shared_release = Arc::clone(&release);
545    let path_hits = Arc::new(Mutex::new(HashMap::<String, usize>::new()));
546    let shared_path_hits = Arc::clone(&path_hits);
547
548    let accept_task = tokio::spawn(async move {
549        while let Ok((mut sock, _)) = listener.accept().await {
550            let counter = Arc::clone(&counter);
551            let routes = Arc::clone(&shared_routes);
552            let fallback = Arc::clone(&fallback);
553            let delay = shared_delay.load(Ordering::SeqCst);
554            let hold = shared_hold.load(Ordering::SeqCst);
555            let release = Arc::clone(&shared_release);
556            let path_hits = Arc::clone(&shared_path_hits);
557            tokio::spawn(async move {
558                if delay > 0 {
559                    tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
560                }
561                // A GET has no body, so end-of-headers is end-of-request.
562                let mut buf = Vec::new();
563                let mut tmp = [0u8; 4096];
564                loop {
565                    match sock.read(&mut tmp).await {
566                        Ok(0) | Err(_) => return,
567                        Ok(n) => buf.extend_from_slice(&tmp[..n]),
568                    }
569                    if buf.windows(4).any(|w| w == b"\r\n\r\n") {
570                        break;
571                    }
572                }
573                counter.fetch_add(1, Ordering::SeqCst);
574                if hold {
575                    release.notified().await;
576                }
577                let request = String::from_utf8_lossy(&buf);
578                let path = request
579                    .lines()
580                    .next()
581                    .and_then(|line| line.split_whitespace().nth(1))
582                    .unwrap_or("/")
583                    .to_string();
584                *path_hits
585                    .lock()
586                    .expect("path hits lock")
587                    .entry(path.clone())
588                    .or_default() += 1;
589                let (status_line, body) = routes
590                    .lock()
591                    .expect("routes lock")
592                    .get(&path)
593                    .cloned()
594                    .or_else(|| (*fallback).clone())
595                    .unwrap_or(("404 Not Found", "{}".to_string()));
596                let resp = format!(
597                    "HTTP/1.1 {status_line}\r\nContent-Type: application/json\r\n\
598                     Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
599                    body.len()
600                );
601                let _ = sock.write_all(resp.as_bytes()).await;
602                let _ = sock.flush().await;
603            });
604        }
605    });
606
607    FakeJwksServer {
608        url: format!("http://{addr}/jwks"),
609        base: format!("http://{addr}"),
610        hits,
611        routes,
612        delay_ms,
613        hold,
614        release,
615        path_hits,
616        accept_task: Mutex::new(Some(accept_task)),
617    }
618}
619
620/// The JWK for [`KEY_B_PEM`]'s public half (`alg` RS256), under [`KID_B`].
621pub fn jwk_rsa_b() -> serde_json::Value {
622    serde_json::json!({
623        "kty": "RSA", "use": "sig", "alg": "RS256", "kid": KID_B, "n": N_B, "e": "AQAB",
624    })
625}
626
627/// Which throwaway RSA key [`TestAuthority`] currently signs RSA tokens with.
628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
629enum RsaKey {
630    A,
631    B,
632}
633
634/// The RSA algorithms other than RS256. Each gets its own JWK (with its own
635/// `alg`, `kid` `<key kid>-<alg lowercase>`, e.g. `test-key-a-ps256`), so no
636/// key in the served set is alg-less and none trips the crate's ambiguous-key
637/// warning.
638const OTHER_RSA_ALGS: [Algorithm; 5] = [
639    Algorithm::RS384,
640    Algorithm::RS512,
641    Algorithm::PS256,
642    Algorithm::PS384,
643    Algorithm::PS512,
644];
645
646impl RsaKey {
647    fn other(self) -> Self {
648        match self {
649            RsaKey::A => RsaKey::B,
650            RsaKey::B => RsaKey::A,
651        }
652    }
653
654    fn pem(self) -> &'static str {
655        match self {
656            RsaKey::A => KEY_A_PEM,
657            RsaKey::B => KEY_B_PEM,
658        }
659    }
660
661    fn n(self) -> &'static str {
662        match self {
663            RsaKey::A => N_A,
664            RsaKey::B => N_B,
665        }
666    }
667
668    fn kid(self) -> &'static str {
669        match self {
670            RsaKey::A => KID_A,
671            RsaKey::B => KID_B,
672        }
673    }
674
675    /// The `kid` this key is published under for `alg`: the bare key `kid` for
676    /// RS256, `<kid>-<alg lowercase>` for the other RSA algorithms.
677    fn kid_for(self, alg: Algorithm) -> String {
678        match alg {
679            Algorithm::RS256 => self.kid().to_string(),
680            other => format!("{}-{}", self.kid(), other.as_str().to_ascii_lowercase()),
681        }
682    }
683
684    /// One JWK per RSA algorithm, each with its own `alg`.
685    fn jwks(self) -> Vec<serde_json::Value> {
686        let mut keys = vec![match self {
687            RsaKey::A => jwk_rsa_a(),
688            RsaKey::B => jwk_rsa_b(),
689        }];
690        for alg in OTHER_RSA_ALGS {
691            keys.push(serde_json::json!({
692                "kty": "RSA", "use": "sig", "alg": alg.as_str(),
693                "kid": self.kid_for(alg), "n": self.n(), "e": "AQAB",
694            }));
695        }
696        keys
697    }
698}
699
700/// Which RSA keys the authority signs with and publishes.
701#[derive(Debug, Clone, Copy)]
702struct KeyState {
703    active: RsaKey,
704    /// Whether the previously active RSA key is still published.
705    retained: bool,
706}
707
708impl KeyState {
709    /// At most 14 keys: 6 per RSA key, plus the P-256 and Ed25519 keys.
710    fn jwks_body(self) -> String {
711        let mut keys = self.active.jwks();
712        if self.retained {
713            keys.extend(self.active.other().jwks());
714        }
715        keys.extend([jwk_ec(), jwk_ed()]);
716        jwks_of(&keys)
717    }
718}
719
720const DISCOVERY_OIDC: &str = "/.well-known/openid-configuration";
721const DISCOVERY_RFC8414: &str = "/.well-known/oauth-authorization-server";
722const JWKS_PATH: &str = "/jwks";
723
724/// A self-consistent fake authorization server for a consumer's tests: a
725/// loopback HTTP server that serves OpenID Connect discovery, RFC 8414
726/// discovery and a JWKS, with an issuer and `jwks_uri` that agree with each
727/// other, plus everything needed to build a matching config and mint matching
728/// tokens.
729///
730/// It publishes the throwaway RSA, P-256 and Ed25519 keys, so a token signed
731/// with any algorithm [`TokenBuilder::alg`] accepts validates. Every published
732/// key carries its own `alg` (one JWK per RSA algorithm, `kid`s such as
733/// `test-key-a` for RS256 and `test-key-a-ps256`), so a validator never logs
734/// the alg-less-key warning for this JWKS.
735///
736/// The server runs until the `TestAuthority` is dropped, which stops its accept
737/// loop.
738///
739/// # Security
740///
741/// Test-only: the keys are public. See the [module docs](self).
742///
743/// # Examples
744///
745/// ```
746/// use oauth_resource_server::OAuthValidator;
747/// use oauth_resource_server::testing::TestAuthority;
748///
749/// # #[tokio::main(flavor = "current_thread")]
750/// # async fn main() {
751/// let authority = TestAuthority::start().await;
752/// // Adjust anything before the config is resolved.
753/// let config = authority.config(|c| c.required_scopes = vec!["notes:write".into()]);
754/// let validator = OAuthValidator::new(&config).unwrap();
755///
756/// let ok = authority.token().scopes(["notes:write"]).sign();
757/// assert!(validator.validate(&ok).await.is_ok());
758/// assert_eq!(authority.jwks_fetches(), 1); // the JWKS was fetched once
759/// # }
760/// ```
761#[derive(Debug)]
762pub struct TestAuthority {
763    server: FakeJwksServer,
764    issuer: String,
765    keys: Mutex<KeyState>,
766}
767
768impl Drop for TestAuthority {
769    fn drop(&mut self) {
770        if let Some(task) = self
771            .server
772            .accept_task
773            .lock()
774            .ok()
775            .and_then(|mut t| t.take())
776        {
777            task.abort();
778        }
779    }
780}
781
782impl TestAuthority {
783    /// The audience [`config`](Self::config) accepts and [`token`](Self::token)
784    /// stamps by default: `https://api.example.test/audience`. Deliberately
785    /// different from [`RESOURCE`](Self::RESOURCE), the way an API identifier
786    /// (RFC 8707 style) differs from the resource's URL in real deployments, so
787    /// a test catches code that swaps the two.
788    pub const AUDIENCE: &'static str = "https://api.example.test/audience";
789    /// The protected resource [`config`](Self::config) uses:
790    /// `https://api.example.test/`.
791    pub const RESOURCE: &'static str = "https://api.example.test/";
792    /// The scope [`config`](Self::config) requires and [`token`](Self::token)
793    /// carries by default: `api:read`.
794    pub const SCOPE: &'static str = "api:read";
795
796    /// Start the fake authority on a loopback port. Its issuer is its own base
797    /// URL (`http://127.0.0.1:<port>`); discovery is served at both
798    /// `/.well-known/openid-configuration` and
799    /// `/.well-known/oauth-authorization-server`, and the JWKS at `/jwks`.
800    ///
801    /// # Panics
802    ///
803    /// If no loopback port can be bound. Must be called inside a tokio runtime.
804    pub async fn start() -> Self {
805        let server = spawn_http_server(HashMap::new(), None).await;
806        let issuer = server.base.clone();
807        let authority = Self {
808            server,
809            issuer,
810            keys: Mutex::new(KeyState {
811                active: RsaKey::A,
812                retained: false,
813            }),
814        };
815        let discovery = serde_json::json!({
816            "issuer": authority.issuer,
817            "jwks_uri": authority.server.url,
818        })
819        .to_string();
820        {
821            let mut routes = authority.server.routes.lock().expect("routes lock");
822            routes.insert(DISCOVERY_OIDC.to_string(), ("200 OK", discovery.clone()));
823            routes.insert(DISCOVERY_RFC8414.to_string(), ("200 OK", discovery));
824        }
825        authority.publish();
826        authority
827    }
828
829    /// The issuer this authority stamps into discovery and, by default, into
830    /// tokens: `http://127.0.0.1:<port>`.
831    pub fn issuer(&self) -> &str {
832        &self.issuer
833    }
834
835    /// The URL of the JWKS: `http://127.0.0.1:<port>/jwks`.
836    pub fn jwks_uri(&self) -> &str {
837        &self.server.url
838    }
839
840    fn path_hits(&self, paths: &[&str]) -> usize {
841        let hits = self.server.path_hits.lock().expect("path hits lock");
842        paths
843            .iter()
844            .map(|p| hits.get(*p).copied().unwrap_or(0))
845            .sum()
846    }
847
848    /// How many times the JWKS has been fetched.
849    pub fn jwks_fetches(&self) -> usize {
850        self.path_hits(&[JWKS_PATH])
851    }
852
853    /// How many times a discovery document (OpenID Connect or RFC 8414) has
854    /// been fetched. Zero when the config sets `jwks_uri`.
855    pub fn discovery_fetches(&self) -> usize {
856        self.path_hits(&[DISCOVERY_OIDC, DISCOVERY_RFC8414])
857    }
858
859    /// Stall every response by `delay`, to test a slow authority. Applies to
860    /// connections accepted after the call; [`Duration::ZERO`] turns it off.
861    pub fn set_response_delay(&self, delay: Duration) {
862        let millis = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX);
863        self.server.delay_ms.store(millis, Ordering::SeqCst);
864    }
865
866    /// Rotate the RSA signing key: from now on [`token`](Self::token) signs RSA
867    /// tokens with the other throwaway key (`KEY_B_PEM` after the first
868    /// rotation, `KEY_A_PEM` again after the second), and the JWKS is
869    /// republished with **both** RSA keys, as a real authority does while
870    /// tokens signed with the old key are still in flight. The P-256 and
871    /// Ed25519 keys are unaffected. Follow with
872    /// [`withdraw_old_key`](Self::withdraw_old_key) to model an authority that
873    /// retires the old key.
874    ///
875    /// A token built *before* the rotation keeps the old key (the builder
876    /// captures the key when [`token`](Self::token) is called). A running
877    /// validator learns the new key on an unknown-`kid` refetch (at most one per
878    /// 60 seconds) or
879    /// [`OAuthValidator::refresh_now`](crate::OAuthValidator::refresh_now).
880    pub fn rotate_key(&self) {
881        {
882            let mut keys = self.keys.lock().expect("key state lock");
883            keys.active = keys.active.other();
884            keys.retained = true;
885        }
886        self.publish();
887    }
888
889    /// Withdraw the previous RSA key that [`rotate_key`](Self::rotate_key) left
890    /// published: the JWKS then holds only the active RSA key. A validator that
891    /// has the old key cached keeps trusting it until its next successful
892    /// refresh ([`refresh_now`](crate::OAuthValidator::refresh_now) or the
893    /// hourly background pass); after that, tokens signed with the old key
894    /// fail. A no-op when nothing is retained.
895    pub fn withdraw_old_key(&self) {
896        self.keys.lock().expect("key state lock").retained = false;
897        self.publish();
898    }
899
900    fn publish(&self) {
901        let body = self.keys.lock().expect("key state lock").jwks_body();
902        self.server
903            .routes
904            .lock()
905            .expect("routes lock")
906            .insert(JWKS_PATH.to_string(), ("200 OK", body));
907    }
908
909    /// A [`ResolvedOAuthConfig`] for this authority, with neutral defaults:
910    ///
911    /// - `issuer` and `jwks_uri` are this authority's own;
912    /// - `audience` is [`AUDIENCE`](Self::AUDIENCE)
913    ///   (`https://api.example.test/audience`) and `resource`
914    ///   [`RESOURCE`](Self::RESOURCE) (`https://api.example.test/`);
915    /// - `api:read` ([`SCOPE`](Self::SCOPE)) is the one required scope;
916    /// - settings are named `oauth.*` in errors ([`KeyNaming::Dotted`]);
917    /// - everything else is the crate default, so `require_at_jwt` is off.
918    ///
919    /// The authority is plain `http` on loopback, which
920    /// [`OAuthConfig::resolve`] accepts without `allow_insecure_http`. `adjust`
921    /// edits the [`OAuthConfig`] before it is resolved: set `require_at_jwt`,
922    /// add `audiences`, clear `jwks_uri` to exercise discovery, and so on.
923    ///
924    /// # Panics
925    ///
926    /// If the adjusted config does not resolve (the message is the
927    /// [`ConfigError`](crate::ConfigError) text, every problem at once), or
928    /// `adjust` sets `enabled` to `false`. Acceptable in a test helper; use
929    /// [`OAuthConfig::resolve`] directly to assert on a refusal.
930    ///
931    /// # Examples
932    ///
933    /// ```
934    /// use oauth_resource_server::testing::TestAuthority;
935    ///
936    /// # #[tokio::main(flavor = "current_thread")]
937    /// # async fn main() {
938    /// let authority = TestAuthority::start().await;
939    /// let config = authority.config(|c| {
940    ///     c.require_at_jwt = true;
941    ///     c.jwks_uri = None; // find the keys through discovery instead
942    /// });
943    /// assert_eq!(config.issuer, authority.issuer());
944    /// assert!(config.jwks_uri.is_none());
945    /// # }
946    /// ```
947    pub fn config(&self, adjust: impl FnOnce(&mut OAuthConfig)) -> ResolvedOAuthConfig {
948        let mut config = OAuthConfig {
949            enabled: true,
950            issuer: self.issuer.clone(),
951            jwks_uri: Some(self.server.url.clone()),
952            audience: Self::AUDIENCE.to_string(),
953            resource: Self::RESOURCE.to_string(),
954            required_scopes: vec![Self::SCOPE.to_string()],
955            ..OAuthConfig::default()
956        };
957        adjust(&mut config);
958        match config.resolve(KeyNaming::Dotted("oauth")) {
959            Ok(Some(resolved)) => resolved,
960            Ok(None) => panic!("TestAuthority::config: the adjusted config has enabled = false"),
961            Err(err) => panic!("TestAuthority::config: the adjusted config is invalid: {err}"),
962        }
963    }
964
965    /// Start a [`TokenBuilder`] whose defaults produce a token the
966    /// [`config`](Self::config) validator accepts: this authority's issuer,
967    /// [`AUDIENCE`](Self::AUDIENCE), subject `test-user`, scope
968    /// [`SCOPE`](Self::SCOPE), valid for an hour, `typ: at+jwt`, signed RS256
969    /// with the currently active RSA key.
970    pub fn token(&self) -> TokenBuilder {
971        let now = now();
972        TokenBuilder {
973            rsa: self.keys.lock().expect("key state lock").active,
974            issuer: self.issuer.clone(),
975            audiences: vec![Self::AUDIENCE.to_string()],
976            subject: "test-user".to_string(),
977            scopes: vec![Self::SCOPE.to_string()],
978            exp: now + 3600,
979            iat: now,
980            nbf: None,
981            typ: Some("at+jwt".to_string()),
982            alg: Algorithm::RS256,
983            kid: None,
984            set: serde_json::Map::new(),
985            removed: Vec::new(),
986        }
987    }
988}
989
990/// A fluent builder for a signed JWT access token, from
991/// [`TestAuthority::token`]. Every method changes one thing about the token, so
992/// a test states exactly what is wrong (or different) about it; the defaults
993/// are documented on [`TestAuthority::token`].
994///
995/// Time setters are relative to now and in seconds ([`expires_in`],
996/// [`expired`], [`not_before_in`], [`issued_ago`]). For an absolute or odd value
997/// use [`claim`](Self::claim): `.claim("nbf", 4102444800_u64)`,
998/// `.claim("exp", "soon")`.
999///
1000/// Claim-shaping calls apply in this order: the fields above (`iss`, `aud`,
1001/// `sub`, `scope`, `exp`, `iat`, `nbf`), then [`claim`](Self::claim) overrides,
1002/// then [`without_claim`](Self::without_claim) removals, so
1003/// `without_claim("exp")` really produces a token with no `exp`.
1004///
1005/// [`expires_in`]: Self::expires_in
1006/// [`expired`]: Self::expired
1007/// [`not_before_in`]: Self::not_before_in
1008/// [`issued_ago`]: Self::issued_ago
1009///
1010/// # Examples
1011///
1012/// ```
1013/// use oauth_resource_server::Algorithm;
1014/// use oauth_resource_server::testing::TestAuthority;
1015///
1016/// # #[tokio::main(flavor = "current_thread")]
1017/// # async fn main() {
1018/// let authority = TestAuthority::start().await;
1019/// let jwt = authority
1020///     .token()
1021///     .subject("ada")
1022///     .scopes(["api:read", "api:write"])
1023///     .audience("https://other.example.test/")
1024///     .expires_in(60)
1025///     .typ("at+jwt")
1026///     .alg(Algorithm::ES256)
1027///     .claim("groups", ["admins"])
1028///     .sign();
1029/// assert_eq!(jwt.split('.').count(), 3);
1030/// # }
1031/// ```
1032#[derive(Debug, Clone)]
1033#[must_use = "a TokenBuilder does nothing until `.sign()` is called"]
1034pub struct TokenBuilder {
1035    rsa: RsaKey,
1036    issuer: String,
1037    audiences: Vec<String>,
1038    subject: String,
1039    scopes: Vec<String>,
1040    exp: u64,
1041    iat: u64,
1042    nbf: Option<u64>,
1043    typ: Option<String>,
1044    alg: Algorithm,
1045    kid: Option<String>,
1046    set: serde_json::Map<String, serde_json::Value>,
1047    removed: Vec<String>,
1048}
1049
1050impl TokenBuilder {
1051    /// Set `sub`.
1052    pub fn subject(mut self, subject: impl Into<String>) -> Self {
1053        self.subject = subject.into();
1054        self
1055    }
1056
1057    /// Replace the scopes, carried in the space-delimited `scope` claim. An
1058    /// empty list omits the claim.
1059    pub fn scopes<I, S>(mut self, scopes: I) -> Self
1060    where
1061        I: IntoIterator<Item = S>,
1062        S: Into<String>,
1063    {
1064        self.scopes = scopes.into_iter().map(Into::into).collect();
1065        self
1066    }
1067
1068    /// Set `aud` to this one audience (a JSON string).
1069    pub fn audience(mut self, audience: impl Into<String>) -> Self {
1070        self.audiences = vec![audience.into()];
1071        self
1072    }
1073
1074    /// Set `aud` to these audiences (a JSON array, or a string when there is
1075    /// exactly one).
1076    pub fn audiences<I, S>(mut self, audiences: I) -> Self
1077    where
1078        I: IntoIterator<Item = S>,
1079        S: Into<String>,
1080    {
1081        self.audiences = audiences.into_iter().map(Into::into).collect();
1082        self
1083    }
1084
1085    /// Override `iss` (default: the authority's issuer), for a wrong-issuer test.
1086    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
1087        self.issuer = issuer.into();
1088        self
1089    }
1090
1091    /// Make the token expire `secs` seconds from now.
1092    pub fn expires_in(mut self, secs: u64) -> Self {
1093        self.exp = now() + secs;
1094        self
1095    }
1096
1097    /// Make the token expired: `exp` an hour in the past, well beyond the
1098    /// default 60-second leeway.
1099    pub fn expired(mut self) -> Self {
1100        self.exp = now().saturating_sub(3600);
1101        self
1102    }
1103
1104    /// Set `nbf` to `secs` seconds in the future, so the token is not yet
1105    /// valid (mind the default 60-second leeway).
1106    pub fn not_before_in(mut self, secs: u64) -> Self {
1107        self.nbf = Some(now() + secs);
1108        self
1109    }
1110
1111    /// Set `iat` to `secs` seconds in the past (default: now).
1112    pub fn issued_ago(mut self, secs: u64) -> Self {
1113        self.iat = now().saturating_sub(secs);
1114        self
1115    }
1116
1117    /// Set the JOSE header `typ` (default `at+jwt`, RFC 9068).
1118    pub fn typ(mut self, typ: impl Into<String>) -> Self {
1119        self.typ = Some(typ.into());
1120        self
1121    }
1122
1123    /// Omit the JOSE header `typ` entirely.
1124    pub fn without_typ(mut self) -> Self {
1125        self.typ = None;
1126        self
1127    }
1128
1129    /// Sign with `alg` (default [`Algorithm::RS256`]), using the matching
1130    /// throwaway key: the active RSA key for RS*/PS*, the P-256 key for ES256,
1131    /// the Ed25519 key for EdDSA. All of them are published by the authority,
1132    /// each under its own `kid`. The header `kid` follows unless
1133    /// [`kid`](Self::kid) overrides it. ES384 has no throwaway key:
1134    /// [`sign`](Self::sign) panics for it.
1135    pub fn alg(mut self, alg: Algorithm) -> Self {
1136        self.alg = alg;
1137        self
1138    }
1139
1140    /// Set the header `kid`, overriding the one that names the signing key
1141    /// (for an unknown-`kid` or "signed by one key, labelled as another" test).
1142    pub fn kid(mut self, kid: impl Into<String>) -> Self {
1143        self.kid = Some(kid.into());
1144        self
1145    }
1146
1147    /// Set (or override) one claim, any JSON-serializable value. This is also
1148    /// how to set an absolute or malformed `exp`/`nbf`/`iat`.
1149    ///
1150    /// # Panics
1151    ///
1152    /// If `value` fails to serialize.
1153    pub fn claim(mut self, name: impl Into<String>, value: impl Serialize) -> Self {
1154        let name = name.into();
1155        self.removed.retain(|removed| *removed != name);
1156        self.set.insert(
1157            name,
1158            serde_json::to_value(value).expect("the claim value serializes"),
1159        );
1160        self
1161    }
1162
1163    /// Leave a claim out of the token, including a default one such as `exp`,
1164    /// `aud` or `iss`.
1165    pub fn without_claim(mut self, name: impl Into<String>) -> Self {
1166        let name = name.into();
1167        self.set.remove(&name);
1168        self.removed.push(name);
1169        self
1170    }
1171
1172    /// Sign the token and return the compact JWT.
1173    ///
1174    /// # Panics
1175    ///
1176    /// If [`alg`](Self::alg) is an algorithm with no throwaway key (ES384).
1177    pub fn sign(self) -> String {
1178        let mut claims = serde_json::Map::new();
1179        claims.insert("iss".into(), self.issuer.clone().into());
1180        claims.insert(
1181            "aud".into(),
1182            match self.audiences.as_slice() {
1183                [one] => one.clone().into(),
1184                many => many.to_vec().into(),
1185            },
1186        );
1187        claims.insert("sub".into(), self.subject.clone().into());
1188        if !self.scopes.is_empty() {
1189            claims.insert("scope".into(), self.scopes.join(" ").into());
1190        }
1191        claims.insert("exp".into(), self.exp.into());
1192        claims.insert("iat".into(), self.iat.into());
1193        if let Some(nbf) = self.nbf {
1194            claims.insert("nbf".into(), nbf.into());
1195        }
1196        claims.extend(self.set.clone());
1197        for name in &self.removed {
1198            claims.remove(name);
1199        }
1200
1201        let (pem, default_kid) = match self.alg {
1202            Algorithm::RS256
1203            | Algorithm::RS384
1204            | Algorithm::RS512
1205            | Algorithm::PS256
1206            | Algorithm::PS384
1207            | Algorithm::PS512 => (self.rsa.pem(), self.rsa.kid_for(self.alg)),
1208            Algorithm::ES256 => (EC_PEM, KID_EC.to_string()),
1209            Algorithm::EdDSA => (ED_PEM, KID_ED.to_string()),
1210            other => panic!("no test key for {other:?}"),
1211        };
1212        let key = match self.alg {
1213            Algorithm::ES256 => EncodingKey::from_ec_pem(pem.as_bytes()),
1214            Algorithm::EdDSA => EncodingKey::from_ed_pem(pem.as_bytes()),
1215            _ => EncodingKey::from_rsa_pem(pem.as_bytes()),
1216        }
1217        .expect("a valid test key");
1218        let mut header = Header::new(self.alg.to_jwt());
1219        header.typ = self.typ.clone();
1220        header.kid = Some(self.kid.clone().unwrap_or(default_kid));
1221        encode(&header, &serde_json::Value::Object(claims), &key).expect("the token encodes")
1222    }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227
1228    use super::*;
1229    use crate::{Credential, OAuthValidator, TokenRejection, authenticate};
1230
1231    fn validator(config: &ResolvedOAuthConfig) -> OAuthValidator {
1232        OAuthValidator::new(config).unwrap()
1233    }
1234
1235    /// No unknown-`kid` cooldown, so a test can observe a refetch without
1236    /// sleeping out the 60 seconds.
1237    fn validator_no_cooldown(config: &ResolvedOAuthConfig) -> OAuthValidator {
1238        OAuthValidator::build(config, Duration::ZERO).unwrap()
1239    }
1240
1241    async fn outcome(
1242        config: &ResolvedOAuthConfig,
1243        token: &str,
1244    ) -> Result<Credential, TokenRejection> {
1245        let v = validator(config);
1246        authenticate([token], None, Some(&v)).await
1247    }
1248
1249    fn is_invalid<T>(r: &Result<T, TokenRejection>) -> bool {
1250        matches!(r, Err(TokenRejection::Invalid(_)))
1251    }
1252
1253    #[tokio::test]
1254    async fn default_token_is_accepted_through_authenticate() {
1255        let authority = TestAuthority::start().await;
1256        let config = authority.config(|_| {});
1257        assert_eq!(config.issuer, authority.issuer());
1258        assert_eq!(config.jwks_uri.as_deref(), Some(authority.jwks_uri()));
1259        assert_eq!(config.audience, TestAuthority::AUDIENCE);
1260        assert_eq!(config.required_scopes, [TestAuthority::SCOPE]);
1261        assert!(!config.allow_insecure_http, "loopback needs no opt-in");
1262
1263        let token = authority.token().sign();
1264        let Ok(Credential::OAuth(t)) = outcome(&config, &token).await else {
1265            panic!("the default token must validate");
1266        };
1267        assert_eq!(t.subject.as_deref(), Some("test-user"));
1268        assert!(t.has_scope("api:read"));
1269        assert_eq!(t.issuer, authority.issuer());
1270        assert_eq!(t.audiences, [TestAuthority::AUDIENCE]);
1271    }
1272
1273    #[tokio::test]
1274    async fn each_builder_knob_changes_the_outcome() {
1275        let authority = TestAuthority::start().await;
1276        let config = authority.config(|c| c.require_at_jwt = true);
1277        let check = |token: String| {
1278            let config = &config;
1279            async move { outcome(config, &token).await }
1280        };
1281
1282        assert!(check(authority.token().sign()).await.is_ok());
1283        assert!(is_invalid(&check(authority.token().expired().sign()).await));
1284        let elsewhere = "https://elsewhere.example.test/";
1285        assert!(is_invalid(
1286            &check(authority.token().audience(elsewhere).sign()).await
1287        ));
1288        assert!(is_invalid(
1289            &check(authority.token().issuer(elsewhere).sign()).await
1290        ));
1291        assert!(matches!(
1292            check(authority.token().scopes(["other:scope"]).sign()).await,
1293            Err(TokenRejection::InsufficientScope)
1294        ));
1295        assert!(is_invalid(
1296            &check(authority.token().typ("JWT").sign()).await
1297        ));
1298        assert!(is_invalid(
1299            &check(authority.token().without_typ().sign()).await
1300        ));
1301        assert!(is_invalid(
1302            &check(authority.token().not_before_in(3600).sign()).await
1303        ));
1304        for claim in ["exp", "aud", "iss"] {
1305            assert!(
1306                is_invalid(&check(authority.token().without_claim(claim).sign()).await),
1307                "a token with no {claim} must be refused"
1308            );
1309        }
1310        // Accepted variations.
1311        let both = authority
1312            .token()
1313            .audiences([elsewhere, TestAuthority::AUDIENCE]);
1314        assert!(check(both.sign()).await.is_ok());
1315        // A past `nbf` (set absolutely through `claim`) and an earlier `iat`.
1316        let past = now() - 10;
1317        assert!(
1318            check(authority.token().claim("nbf", past).issued_ago(10).sign())
1319                .await
1320                .is_ok()
1321        );
1322        assert!(check(authority.token().expires_in(30).sign()).await.is_ok());
1323        // An unknown `kid` is refused: no such key in the JWKS.
1324        assert!(is_invalid(
1325            &check(authority.token().kid("no-such-key").sign()).await
1326        ));
1327    }
1328
1329    #[tokio::test]
1330    async fn claim_and_without_claim_shape_the_claims() {
1331        let authority = TestAuthority::start().await;
1332        let v = validator(&authority.config(|_| {}));
1333        let token = authority
1334            .token()
1335            .subject("ada")
1336            .claim("groups", ["admins"])
1337            .claim("email", "ada@example.test")
1338            .without_claim("email")
1339            .claim("azp", "client-1")
1340            .sign();
1341        let t = v.validate(&token).await.unwrap();
1342        assert_eq!(t.subject.as_deref(), Some("ada"));
1343        assert_eq!(t.client_id.as_deref(), Some("client-1"));
1344        assert_eq!(t.claims()["groups"], serde_json::json!(["admins"]));
1345        assert!(!t.claims().contains_key("email"));
1346        // `claim` after `without_claim` puts it back.
1347        let token = authority
1348            .token()
1349            .without_claim("sub")
1350            .claim("sub", "back")
1351            .sign();
1352        let t = v.validate(&token).await.unwrap();
1353        assert_eq!(t.subject.as_deref(), Some("back"));
1354    }
1355
1356    #[tokio::test]
1357    async fn every_algorithm_with_a_test_key_validates() {
1358        let authority = TestAuthority::start().await;
1359        let v = validator(&authority.config(|_| {}));
1360        for alg in [
1361            Algorithm::RS256,
1362            Algorithm::RS384,
1363            Algorithm::RS512,
1364            Algorithm::PS256,
1365            Algorithm::PS384,
1366            Algorithm::PS512,
1367            Algorithm::ES256,
1368            Algorithm::EdDSA,
1369        ] {
1370            let token = authority.token().alg(alg).sign();
1371            assert!(v.validate(&token).await.is_ok(), "{alg:?} must validate");
1372        }
1373    }
1374
1375    #[tokio::test]
1376    #[should_panic(expected = "no test key for ES384")]
1377    async fn an_algorithm_without_a_test_key_panics() {
1378        let authority = TestAuthority::start().await;
1379        let _ = authority.token().alg(Algorithm::ES384).sign();
1380    }
1381
1382    #[tokio::test]
1383    async fn rotate_key_publishes_both_keys_and_signs_with_the_new_one() {
1384        let authority = TestAuthority::start().await;
1385        let v = validator_no_cooldown(&authority.config(|_| {}));
1386        let old = authority.token().sign();
1387        assert!(v.validate(&old).await.is_ok());
1388        let before = authority.jwks_fetches();
1389
1390        authority.rotate_key();
1391        let new = authority.token().sign();
1392        // Signed with the other key, under the other `kid`.
1393        assert_ne!(old.split('.').next(), new.split('.').next());
1394        // The unknown `kid` triggers a refetch, which finds the new key...
1395        assert!(v.validate(&new).await.is_ok());
1396        assert!(
1397            authority.jwks_fetches() > before,
1398            "the rotation was refetched"
1399        );
1400        // ...and the old key is still published, so the old token still works.
1401        assert!(v.validate(&old).await.is_ok());
1402
1403        // A second rotation returns to the first key.
1404        authority.rotate_key();
1405        let again = authority.token().sign();
1406        assert_eq!(old.split('.').next(), again.split('.').next());
1407    }
1408
1409    #[tokio::test]
1410    async fn withdrawing_the_old_key_fails_its_tokens_after_a_refresh() {
1411        let authority = TestAuthority::start().await;
1412        let v = validator_no_cooldown(&authority.config(|_| {}));
1413        let old = authority.token().sign();
1414        assert!(v.validate(&old).await.is_ok());
1415
1416        authority.rotate_key();
1417        authority.withdraw_old_key();
1418        // Fail-closed caching: the withdrawn key stays trusted until a refresh.
1419        assert!(v.validate(&old).await.is_ok());
1420        v.refresh_now().await.unwrap();
1421        assert!(is_invalid(&v.validate(&old).await));
1422        assert!(v.validate(&authority.token().sign()).await.is_ok());
1423    }
1424
1425    #[tokio::test]
1426    async fn withdraw_old_key_drops_the_retained_key() {
1427        let authority = TestAuthority::start().await;
1428        let v = validator_no_cooldown(&authority.config(|_| {}));
1429        let old = authority.token().sign();
1430        authority.rotate_key();
1431        v.refresh_now().await.unwrap();
1432        assert!(v.validate(&old).await.is_ok(), "retained after rotate_key");
1433        authority.withdraw_old_key();
1434        v.refresh_now().await.unwrap();
1435        assert!(is_invalid(&v.validate(&old).await));
1436        assert!(v.validate(&authority.token().sign()).await.is_ok());
1437    }
1438
1439    #[tokio::test]
1440    async fn discovery_finds_the_jwks_through_either_document() {
1441        let authority = TestAuthority::start().await;
1442        let config = authority.config(|c| c.jwks_uri = None);
1443        assert!(config.jwks_uri.is_none());
1444        let v = validator(&config);
1445        assert!(v.validate(&authority.token().sign()).await.is_ok());
1446
1447        assert!(authority.discovery_fetches() >= 1);
1448        assert_eq!(authority.jwks_fetches(), 1);
1449
1450        // With the OIDC document gone, RFC 8414 discovery still works.
1451        authority
1452            .server
1453            .routes
1454            .lock()
1455            .unwrap()
1456            .remove(DISCOVERY_OIDC);
1457        let v = validator(&config);
1458        let es256 = authority.token().alg(Algorithm::ES256).sign();
1459        assert!(v.validate(&es256).await.is_ok());
1460    }
1461
1462    #[tokio::test]
1463    async fn the_served_jwks_has_no_alg_less_key_and_stays_under_the_key_cap() {
1464        let authority = TestAuthority::start().await;
1465        let all: Vec<Algorithm> = DEFAULT_ALGORITHMS
1466            .iter()
1467            .map(|a| parse_algorithm(a).unwrap())
1468            .collect();
1469        for retained in [false, true] {
1470            let body = KeyState {
1471                active: RsaKey::A,
1472                retained,
1473            }
1474            .jwks_body();
1475            let jwks: serde_json::Value = serde_json::from_str(&body).unwrap();
1476            let keys = jwks["keys"].as_array().unwrap();
1477            assert!(keys.len() <= crate::jwks::MAX_JWKS_KEYS);
1478            for entry in keys {
1479                let key = crate::jwks::parse_jwks_entry(entry, &all)
1480                    .unwrap_or_else(|| panic!("usable key expected: {entry}"));
1481                assert!(!key.ambiguous, "the crate would warn about {entry}");
1482            }
1483        }
1484        // And what the live server serves is the same document.
1485        let served = authority
1486            .server
1487            .routes
1488            .lock()
1489            .unwrap()
1490            .get(JWKS_PATH)
1491            .cloned()
1492            .unwrap()
1493            .1;
1494        assert_eq!(
1495            served,
1496            KeyState {
1497                active: RsaKey::A,
1498                retained: false
1499            }
1500            .jwks_body()
1501        );
1502    }
1503
1504    #[tokio::test]
1505    async fn dropping_the_authority_stops_its_server() {
1506        let authority = TestAuthority::start().await;
1507        let addr = authority
1508            .server
1509            .base
1510            .trim_start_matches("http://")
1511            .to_string();
1512        assert!(tokio::net::TcpStream::connect(&addr).await.is_ok());
1513        drop(authority);
1514        let mut stopped = false;
1515        for _ in 0..100 {
1516            if tokio::net::TcpStream::connect(&addr).await.is_err() {
1517                stopped = true;
1518                break;
1519            }
1520            tokio::time::sleep(Duration::from_millis(20)).await;
1521        }
1522        assert!(stopped, "the accept loop must end with the authority");
1523    }
1524
1525    #[tokio::test]
1526    async fn a_response_delay_stalls_the_authority() {
1527        let authority = TestAuthority::start().await;
1528        authority.set_response_delay(Duration::from_millis(300));
1529        let started = std::time::Instant::now();
1530        let v = validator(&authority.config(|_| {}));
1531        assert!(v.validate(&authority.token().sign()).await.is_ok());
1532        assert!(started.elapsed() >= Duration::from_millis(300));
1533    }
1534
1535    #[tokio::test]
1536    #[should_panic(expected = "the adjusted config is invalid")]
1537    async fn config_panics_with_the_resolve_error() {
1538        let authority = TestAuthority::start().await;
1539        let _ = authority.config(|c| c.audience = String::new());
1540    }
1541
1542    #[cfg(feature = "axum")]
1543    #[tokio::test]
1544    async fn axum_end_to_end_with_layer_and_extractor() {
1545        use std::sync::Arc;
1546
1547        use ::axum::Router;
1548        use ::axum::body::Body;
1549        use ::axum::http::{Request, StatusCode};
1550        use ::axum::routing::get;
1551        use tower::ServiceExt;
1552
1553        use crate::AuthorizedToken;
1554        use crate::axum::AuthLayer;
1555
1556        let authority = TestAuthority::start().await;
1557        let validator = Arc::new(OAuthValidator::new(&authority.config(|_| {})).unwrap());
1558        let layer = AuthLayer::builder().oauth(validator).build().unwrap();
1559        let app = Router::new()
1560            .route(
1561                "/whoami",
1562                get(|token: AuthorizedToken| async move { token.subject.unwrap_or_default() }),
1563            )
1564            .route_layer(layer);
1565        let call = |bearer: Option<String>| {
1566            let app = app.clone();
1567            async move {
1568                let mut req = Request::builder().uri("/whoami");
1569                if let Some(bearer) = bearer {
1570                    req = req.header("authorization", format!("Bearer {bearer}"));
1571                }
1572                app.oneshot(req.body(Body::empty()).unwrap()).await.unwrap()
1573            }
1574        };
1575
1576        let resp = call(Some(authority.token().subject("ada").sign())).await;
1577        assert_eq!(resp.status(), StatusCode::OK);
1578        let body = ::axum::body::to_bytes(resp.into_body(), 1024)
1579            .await
1580            .unwrap();
1581        assert_eq!(body, "ada");
1582
1583        let resp = call(Some(authority.token().expired().sign())).await;
1584        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1585        assert!(resp.headers().contains_key("www-authenticate"));
1586        let resp = call(Some(authority.token().scopes(["other:scope"]).sign())).await;
1587        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1588        let resp = call(None).await;
1589        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1590    }
1591}