1use 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
189pub const KID_A: &str = "test-key-a";
192pub const N_A: &str = "zXtrd9E8iuVecx_7KN0nxRV0m0DgZayGgW5D4bPJMwUcFX6SIsyYpSCAGjT1Fia85xH-YrMxk9XSjuMpYB8GphQ5NitAaVx8CQeoVQw8WEi1YSG53OfuSftmkX79D48nVP6VxKq3JW_RIaTM8xsisVV2zzFeQVN_NsFNCAsClYoXLUj8Wfc9WsFz8DszbQep6I4gceD6WNCs72AQMXR5vIOfGxK5eP5JWOjK7FN95njVNbXY6p5QUQii_3HkFSDQv9drzpzeKXdDziFdSG5qZfMwGuqjfCMDNfwYKxC4AbAGbtSTCHFEWe0CuWX95xgqvyJCsVjkh8xMz-WpPoWLSQ";
194
195pub 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
226pub 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";
258pub const KID_B: &str = "test-key-b";
260pub const N_B: &str = "pcHroF0xdUmNKmq_GevKJ4PXgsziN5Akoqbogg4VT2nfBp-G3tETOAe7d-NorhuFI8l5wkKQqFhojS-CjedoRQROzPHP-ybrQLZqSRZt93BSmfslnwHF-RRbQvwXTj32j7Yo3vfECUZpb5Sep-3PLjKV4bVdWSKsJvYO3IVVpVBygIDBRmlHilnZLEPbU2YAynhztb30WWZWE13bw6TXuw8c9aN3nn07KyrBNQw016w3yT1Azz6QUzJk_saz0XobjaqzUEVCVhCAEWyaVeZbZ4TV-M7N2EN_f10wnEI527uMEaQ1IEyIT7qJoNfLfL6otQm4WjfPlsN6FBUXVyhOfQ";
263
264pub const KID_EC: &str = "test-key-ec";
268pub const EC_PEM: &str = "-----BEGIN PRIVATE KEY-----
270MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgt+Eh+ZhHxw1rLcOh
271VFTMghCKj2Vjq7F3zWwemIamL62hRANCAATCkjHQ5M6RrM1TPQ6wuvqltcwRa4AL
272s/Jd92N5PXaKwn94PezTTY6vFt/ivjcfSSG5wWncUlc92lsipOXRqgLZ
273-----END PRIVATE KEY-----
274";
275pub const EC_X: &str = "wpIx0OTOkazNUz0OsLr6pbXMEWuAC7PyXfdjeT12isI";
277pub const EC_Y: &str = "f3g97NNNjq8W3-K-Nx9JIbnBadxSVz3aWyKk5dGqAtk";
279
280pub const KID_ED: &str = "test-key-ed";
283pub const ED_PEM: &str = "-----BEGIN PRIVATE KEY-----
285MC4CAQAwBQYDK2VwBCIEICpZSYX0J1AafpNnoaSXF7Lm/Nmt73HqecXyoFLjchf8
286-----END PRIVATE KEY-----
287";
288pub const ED_X: &str = "C4HUUU7zy0ZEyY__PV16YbPgh4b4clhBg0oVMg0_EaQ";
290
291pub const ISSUER: &str = "https://authentik.example.test/application/o/example-app/";
295pub const AUDIENCE: &str = "test-client-id";
297pub const RESOURCE: &str = "https://kb.example.test/mcp";
299
300pub 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
308pub 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
313pub 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
321pub 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
329pub fn jwks_of(keys: &[serde_json::Value]) -> String {
331 serde_json::json!({ "keys": keys }).to_string()
332}
333
334pub fn jwks_body() -> String {
337 jwks_of(&[jwk_rsa_a()])
338}
339
340pub 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
351pub 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
391pub 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
403pub 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
424pub 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
456pub 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#[derive(Debug)]
483#[non_exhaustive]
484pub struct FakeJwksServer {
485 pub url: String,
487 pub base: String,
489 pub hits: Arc<AtomicUsize>,
491 pub routes: Arc<Mutex<HashMap<String, (&'static str, String)>>>,
493 pub delay_ms: Arc<AtomicU64>,
496 #[cfg_attr(not(test), allow(dead_code))]
501 pub(crate) hold: Arc<AtomicBool>,
502 #[cfg_attr(not(test), allow(dead_code))]
504 pub(crate) release: Arc<tokio::sync::Notify>,
505 pub(crate) path_hits: Arc<Mutex<HashMap<String, usize>>>,
507 pub(crate) accept_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
509}
510
511pub 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
520pub 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 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
620pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
629enum RsaKey {
630 A,
631 B,
632}
633
634const 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 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 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#[derive(Debug, Clone, Copy)]
702struct KeyState {
703 active: RsaKey,
704 retained: bool,
706}
707
708impl KeyState {
709 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#[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 pub const AUDIENCE: &'static str = "https://api.example.test/audience";
789 pub const RESOURCE: &'static str = "https://api.example.test/";
792 pub const SCOPE: &'static str = "api:read";
795
796 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 pub fn issuer(&self) -> &str {
832 &self.issuer
833 }
834
835 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 pub fn jwks_fetches(&self) -> usize {
850 self.path_hits(&[JWKS_PATH])
851 }
852
853 pub fn discovery_fetches(&self) -> usize {
856 self.path_hits(&[DISCOVERY_OIDC, DISCOVERY_RFC8414])
857 }
858
859 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 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 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 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 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#[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 pub fn subject(mut self, subject: impl Into<String>) -> Self {
1053 self.subject = subject.into();
1054 self
1055 }
1056
1057 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 pub fn audience(mut self, audience: impl Into<String>) -> Self {
1070 self.audiences = vec![audience.into()];
1071 self
1072 }
1073
1074 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 pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
1087 self.issuer = issuer.into();
1088 self
1089 }
1090
1091 pub fn expires_in(mut self, secs: u64) -> Self {
1093 self.exp = now() + secs;
1094 self
1095 }
1096
1097 pub fn expired(mut self) -> Self {
1100 self.exp = now().saturating_sub(3600);
1101 self
1102 }
1103
1104 pub fn not_before_in(mut self, secs: u64) -> Self {
1107 self.nbf = Some(now() + secs);
1108 self
1109 }
1110
1111 pub fn issued_ago(mut self, secs: u64) -> Self {
1113 self.iat = now().saturating_sub(secs);
1114 self
1115 }
1116
1117 pub fn typ(mut self, typ: impl Into<String>) -> Self {
1119 self.typ = Some(typ.into());
1120 self
1121 }
1122
1123 pub fn without_typ(mut self) -> Self {
1125 self.typ = None;
1126 self
1127 }
1128
1129 pub fn alg(mut self, alg: Algorithm) -> Self {
1136 self.alg = alg;
1137 self
1138 }
1139
1140 pub fn kid(mut self, kid: impl Into<String>) -> Self {
1143 self.kid = Some(kid.into());
1144 self
1145 }
1146
1147 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 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 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 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 let both = authority
1312 .token()
1313 .audiences([elsewhere, TestAuthority::AUDIENCE]);
1314 assert!(check(both.sign()).await.is_ok());
1315 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 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 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 assert_ne!(old.split('.').next(), new.split('.').next());
1394 assert!(v.validate(&new).await.is_ok());
1396 assert!(
1397 authority.jwks_fetches() > before,
1398 "the rotation was refetched"
1399 );
1400 assert!(v.validate(&old).await.is_ok());
1402
1403 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 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 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 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}