1use std::collections::HashMap;
16
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct AuthSubject {
21 pub name: String,
23 pub groups: Vec<String>,
25 pub claims: HashMap<String, String>,
27 pub fingerprint: Option<String>,
31}
32
33impl AuthSubject {
34 #[must_use]
36 pub fn anonymous() -> Self {
37 Self::new("anonymous")
38 }
39
40 #[must_use]
42 pub fn new(name: impl Into<String>) -> Self {
43 Self {
44 name: name.into(),
45 groups: Vec::new(),
46 claims: HashMap::new(),
47 fingerprint: None,
48 }
49 }
50
51 #[must_use]
53 pub fn with_group(mut self, g: impl Into<String>) -> Self {
54 self.groups.push(g.into());
55 self
56 }
57
58 #[must_use]
60 pub fn with_claim(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
61 self.claims.insert(k.into(), v.into());
62 self
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum AuthError {
69 MissingCredentials,
71 MalformedCredentials(String),
73 Rejected(String),
75 Misconfigured(String),
77}
78
79impl core::fmt::Display for AuthError {
80 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81 match self {
82 Self::MissingCredentials => f.write_str("missing credentials"),
83 Self::MalformedCredentials(m) => write!(f, "malformed credentials: {m}"),
84 Self::Rejected(m) => write!(f, "credentials rejected: {m}"),
85 Self::Misconfigured(m) => write!(f, "auth misconfigured: {m}"),
86 }
87 }
88}
89
90impl std::error::Error for AuthError {}
91
92#[derive(Debug, Clone)]
94pub enum AuthMode {
95 None,
97 Bearer {
100 tokens: HashMap<String, AuthSubject>,
102 },
103 Jwt {
106 pkcs1_pubkey_der: Vec<u8>,
108 expected_issuer: Option<String>,
110 expected_audience: Option<String>,
113 required_claim: Option<(String, String)>,
117 },
118 Mtls,
122 SaslPlain {
125 users: HashMap<String, String>,
127 },
128}
129
130#[derive(Debug, Clone, Default)]
132pub struct AuthInput<'a> {
133 pub authorization_header: Option<&'a str>,
135 pub sasl_plain_blob: Option<&'a [u8]>,
137 pub mtls_subject: Option<AuthSubject>,
140}
141
142impl AuthMode {
143 pub fn validate(&self, input: &AuthInput<'_>) -> Result<AuthSubject, AuthError> {
148 match self {
149 Self::None => Ok(AuthSubject::anonymous()),
150 Self::Bearer { tokens } => {
151 let hdr = input
152 .authorization_header
153 .ok_or(AuthError::MissingCredentials)?;
154 let token = strip_bearer(hdr)?;
155 tokens
156 .get(token)
157 .cloned()
158 .ok_or_else(|| AuthError::Rejected("unknown bearer token".to_string()))
159 }
160 Self::Jwt {
161 pkcs1_pubkey_der,
162 expected_issuer,
163 expected_audience,
164 required_claim,
165 } => {
166 let hdr = input
167 .authorization_header
168 .ok_or(AuthError::MissingCredentials)?;
169 let token = strip_bearer(hdr)?;
170 validate_jwt_rs256(
171 token,
172 pkcs1_pubkey_der,
173 &JwtChecks {
174 issuer: expected_issuer.as_deref(),
175 audience: expected_audience.as_deref(),
176 required_claim: required_claim
177 .as_ref()
178 .map(|(k, v)| (k.as_str(), v.as_str())),
179 now: unix_now(),
180 },
181 )
182 }
183 Self::Mtls => input
184 .mtls_subject
185 .clone()
186 .ok_or_else(|| AuthError::Rejected("mTLS expected client cert".to_string())),
187 Self::SaslPlain { users } => {
188 let blob = input.sasl_plain_blob.ok_or(AuthError::MissingCredentials)?;
189 let (user, pass) = parse_sasl_plain(blob)?;
190 let stored = users
191 .get(user)
192 .ok_or_else(|| AuthError::Rejected("unknown user".to_string()))?;
193 if stored == pass {
194 Ok(AuthSubject::new(user))
195 } else {
196 Err(AuthError::Rejected("password mismatch".to_string()))
197 }
198 }
199 }
200 }
201}
202
203fn strip_bearer(hdr: &str) -> Result<&str, AuthError> {
204 let trimmed = hdr.trim();
205 let prefix = "Bearer ";
206 if trimmed.len() < prefix.len()
207 || !trimmed
208 .get(..prefix.len())
209 .is_some_and(|p| p.eq_ignore_ascii_case(prefix))
210 {
211 return Err(AuthError::MalformedCredentials(
212 "expected `Bearer …`".to_string(),
213 ));
214 }
215 Ok(trimmed[prefix.len()..].trim())
216}
217
218fn parse_sasl_plain(blob: &[u8]) -> Result<(&str, &str), AuthError> {
219 let mut parts = blob.splitn(3, |b| *b == 0);
221 let _authzid = parts
222 .next()
223 .ok_or(AuthError::MalformedCredentials("sasl-plain empty".into()))?;
224 let authcid = parts
225 .next()
226 .ok_or(AuthError::MalformedCredentials("sasl-plain no user".into()))?;
227 let passwd = parts
228 .next()
229 .ok_or(AuthError::MalformedCredentials("sasl-plain no pass".into()))?;
230 let user = core::str::from_utf8(authcid)
231 .map_err(|_| AuthError::MalformedCredentials("sasl-plain user utf8".into()))?;
232 let pass = core::str::from_utf8(passwd)
233 .map_err(|_| AuthError::MalformedCredentials("sasl-plain pass utf8".into()))?;
234 if user.is_empty() {
235 return Err(AuthError::MalformedCredentials(
236 "sasl-plain empty user".into(),
237 ));
238 }
239 Ok((user, pass))
240}
241
242pub const JWT_CLOCK_LEEWAY_SECS: u64 = 60;
248
249struct JwtChecks<'a> {
251 issuer: Option<&'a str>,
252 audience: Option<&'a str>,
253 required_claim: Option<(&'a str, &'a str)>,
254 now: u64,
256}
257
258fn unix_now() -> u64 {
259 std::time::SystemTime::now()
260 .duration_since(std::time::UNIX_EPOCH)
261 .map_or(0, |d| d.as_secs())
262}
263
264fn numeric_date(payload: &str, key: &str) -> Result<Option<u64>, AuthError> {
266 let Some(raw) = json_field(payload, key) else {
267 return Ok(None);
268 };
269 let whole = raw.split('.').next().unwrap_or("");
270 whole
271 .parse::<u64>()
272 .map(Some)
273 .map_err(|_| AuthError::MalformedCredentials(format!("jwt: {key} is not a NumericDate")))
274}
275
276fn validate_jwt_rs256(
277 token: &str,
278 pkcs1_pubkey_der: &[u8],
279 checks: &JwtChecks<'_>,
280) -> Result<AuthSubject, AuthError> {
281 use base64::Engine as _;
282 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
283
284 let mut parts = token.split('.');
285 let h_b64 = parts
286 .next()
287 .ok_or_else(|| AuthError::MalformedCredentials("jwt: no header".into()))?;
288 let p_b64 = parts
289 .next()
290 .ok_or_else(|| AuthError::MalformedCredentials("jwt: no payload".into()))?;
291 let s_b64 = parts
292 .next()
293 .ok_or_else(|| AuthError::MalformedCredentials("jwt: no sig".into()))?;
294 if parts.next().is_some() {
295 return Err(AuthError::MalformedCredentials(
296 "jwt: too many segments".into(),
297 ));
298 }
299
300 let header_bytes = engine
301 .decode(h_b64)
302 .map_err(|e| AuthError::MalformedCredentials(format!("jwt header b64: {e}")))?;
303 let payload_bytes = engine
304 .decode(p_b64)
305 .map_err(|e| AuthError::MalformedCredentials(format!("jwt payload b64: {e}")))?;
306 let sig_bytes = engine
307 .decode(s_b64)
308 .map_err(|e| AuthError::MalformedCredentials(format!("jwt sig b64: {e}")))?;
309
310 let header_str = core::str::from_utf8(&header_bytes)
312 .map_err(|_| AuthError::MalformedCredentials("jwt header utf8".into()))?;
313 if !json_field_eq(header_str, "alg", "RS256") {
314 return Err(AuthError::Rejected("jwt: alg must be RS256".into()));
315 }
316
317 let signed = {
319 let mut v = Vec::with_capacity(h_b64.len() + 1 + p_b64.len());
320 v.extend_from_slice(h_b64.as_bytes());
321 v.push(b'.');
322 v.extend_from_slice(p_b64.as_bytes());
323 v
324 };
325 let pubkey = crate::backend::signature::UnparsedPublicKey::new(
326 &crate::backend::signature::RSA_PKCS1_2048_8192_SHA256,
327 pkcs1_pubkey_der,
328 );
329 pubkey
330 .verify(&signed, &sig_bytes)
331 .map_err(|_| AuthError::Rejected("jwt: signature invalid".into()))?;
332
333 let payload_str = core::str::from_utf8(&payload_bytes)
335 .map_err(|_| AuthError::MalformedCredentials("jwt payload utf8".into()))?;
336 let sub = json_field(payload_str, "sub")
337 .ok_or_else(|| AuthError::Rejected("jwt: no sub claim".into()))?;
338
339 if let Some(exp) = numeric_date(payload_str, "exp")? {
342 if checks.now >= exp.saturating_add(JWT_CLOCK_LEEWAY_SECS) {
343 return Err(AuthError::Rejected("jwt: expired".into()));
344 }
345 }
346 if let Some(nbf) = numeric_date(payload_str, "nbf")? {
347 if checks.now.saturating_add(JWT_CLOCK_LEEWAY_SECS) < nbf {
348 return Err(AuthError::Rejected("jwt: not yet valid".into()));
349 }
350 }
351
352 if let Some(expected) = checks.issuer {
353 let iss = json_field(payload_str, "iss")
354 .ok_or_else(|| AuthError::Rejected("jwt: no iss claim".into()))?;
355 if iss != expected {
356 return Err(AuthError::Rejected(format!("jwt: iss != {expected}")));
357 }
358 }
359 if let Some(expected) = checks.audience {
360 let matches = match json_array(payload_str, "aud") {
361 Some(list) => list.iter().any(|a| a == expected),
362 None => json_field(payload_str, "aud").is_some_and(|a| a == expected),
363 };
364 if !matches {
365 return Err(AuthError::Rejected(format!("jwt: aud lacks {expected}")));
366 }
367 }
368 if let Some((claim, value)) = checks.required_claim {
369 let matches = match json_array(payload_str, claim) {
370 Some(list) => list.iter().any(|v| v == value),
371 None => json_field(payload_str, claim)
372 .is_some_and(|v| v == value || v.split(' ').any(|p| p == value)),
373 };
374 if !matches {
375 return Err(AuthError::Rejected(format!(
376 "jwt: claim {claim} lacks {value}"
377 )));
378 }
379 }
380
381 let mut subj = AuthSubject::new(sub);
382 if let Some(groups_raw) = json_array(payload_str, "groups") {
383 for g in groups_raw {
384 subj.groups.push(g);
385 }
386 }
387 Ok(subj)
388}
389
390pub fn jwt_public_key_from_pem(pem: &str) -> Result<Vec<u8>, String> {
401 use base64::Engine as _;
402 fn block(pem: &str, label: &str) -> Option<String> {
403 let begin = format!("-----BEGIN {label}-----");
404 let end = format!("-----END {label}-----");
405 let start = pem.find(&begin)? + begin.len();
406 let stop = pem[start..].find(&end)? + start;
407 Some(pem[start..stop].split_whitespace().collect())
408 }
409 let decode = |b64: String| {
410 base64::engine::general_purpose::STANDARD
411 .decode(b64)
412 .map_err(|e| format!("pem base64: {e}"))
413 };
414 if let Some(b64) = block(pem, "RSA PUBLIC KEY") {
415 return decode(b64);
416 }
417 let Some(b64) = block(pem, "PUBLIC KEY") else {
418 return Err("no RSA PUBLIC KEY or PUBLIC KEY block".into());
419 };
420 let spki = decode(b64)?;
421 let (tag, outer, _) = der_tlv(&spki).ok_or("spki: bad outer TLV")?;
423 if tag != 0x30 {
424 return Err("spki: not a SEQUENCE".into());
425 }
426 let (tag, alg, rest) = der_tlv(outer).ok_or("spki: bad algorithm TLV")?;
427 if tag != 0x30 {
428 return Err("spki: no AlgorithmIdentifier".into());
429 }
430 let (tag, oid, _) = der_tlv(alg).ok_or("spki: bad OID TLV")?;
431 const RSA_ENCRYPTION: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
432 if tag != 0x06 || oid != RSA_ENCRYPTION {
433 return Err("spki: algorithm is not rsaEncryption".into());
434 }
435 let (tag, bits, _) = der_tlv(rest).ok_or("spki: bad BIT STRING TLV")?;
436 match (tag, bits.split_first()) {
437 (0x03, Some((0, key))) => Ok(key.to_vec()),
438 _ => Err("spki: no BIT STRING with the key".into()),
439 }
440}
441
442fn der_tlv(input: &[u8]) -> Option<(u8, &[u8], &[u8])> {
444 let (&tag, rest) = input.split_first()?;
445 let (&first, rest) = rest.split_first()?;
446 let (len, rest) = if first < 0x80 {
447 (usize::from(first), rest)
448 } else {
449 let n = usize::from(first & 0x7f);
450 if n == 0 || n > 4 || rest.len() < n {
451 return None;
452 }
453 let len = rest[..n]
454 .iter()
455 .fold(0usize, |acc, b| (acc << 8) | usize::from(*b));
456 (len, &rest[n..])
457 };
458 if rest.len() < len {
459 return None;
460 }
461 Some((tag, &rest[..len], &rest[len..]))
462}
463
464fn json_field(src: &str, key: &str) -> Option<String> {
469 let pat = format!("\"{key}\"");
470 let pos = src.find(&pat)?;
471 let after = &src[pos + pat.len()..];
472 let colon = after.find(':')?;
473 let rest = after[colon + 1..].trim_start();
474 if let Some(stripped) = rest.strip_prefix('"') {
475 let end = stripped.find('"')?;
476 Some(stripped[..end].to_string())
477 } else {
478 let end = rest
480 .find(|c: char| c == ',' || c == '}' || c.is_whitespace())
481 .unwrap_or(rest.len());
482 Some(rest[..end].to_string())
483 }
484}
485
486fn json_field_eq(src: &str, key: &str, expected: &str) -> bool {
487 json_field(src, key).is_some_and(|v| v == expected)
488}
489
490fn json_array(src: &str, key: &str) -> Option<Vec<String>> {
491 let pat = format!("\"{key}\"");
492 let pos = src.find(&pat)?;
493 let after = &src[pos + pat.len()..];
494 let colon = after.find(':')?;
495 let rest = after[colon + 1..].trim_start();
496 let stripped = rest.strip_prefix('[')?;
497 let end = stripped.find(']')?;
498 let inside = &stripped[..end];
499 let mut out = Vec::new();
500 for piece in inside.split(',') {
501 let p = piece.trim().trim_matches('"');
502 if !p.is_empty() {
503 out.push(p.to_string());
504 }
505 }
506 Some(out)
507}
508
509#[cfg(test)]
510#[allow(clippy::expect_used, clippy::unwrap_used)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn none_mode_yields_anonymous() {
516 let m = AuthMode::None;
517 let s = m.validate(&AuthInput::default()).unwrap();
518 assert_eq!(s.name, "anonymous");
519 }
520
521 #[test]
522 fn bearer_valid_token_accepted() {
523 let mut tokens = HashMap::new();
524 tokens.insert("secret123".to_string(), AuthSubject::new("alice"));
525 let m = AuthMode::Bearer { tokens };
526 let s = m
527 .validate(&AuthInput {
528 authorization_header: Some("Bearer secret123"),
529 ..Default::default()
530 })
531 .unwrap();
532 assert_eq!(s.name, "alice");
533 }
534
535 #[test]
536 fn bearer_invalid_token_rejected() {
537 let m = AuthMode::Bearer {
538 tokens: HashMap::new(),
539 };
540 let err = m
541 .validate(&AuthInput {
542 authorization_header: Some("Bearer wrong"),
543 ..Default::default()
544 })
545 .unwrap_err();
546 assert!(matches!(err, AuthError::Rejected(_)));
547 }
548
549 #[test]
550 fn bearer_missing_header_returns_missing() {
551 let m = AuthMode::Bearer {
552 tokens: HashMap::new(),
553 };
554 let err = m.validate(&AuthInput::default()).unwrap_err();
555 assert!(matches!(err, AuthError::MissingCredentials));
556 }
557
558 #[test]
559 fn bearer_malformed_header_returns_malformed() {
560 let m = AuthMode::Bearer {
561 tokens: HashMap::new(),
562 };
563 let err = m
564 .validate(&AuthInput {
565 authorization_header: Some("Basic xx"),
566 ..Default::default()
567 })
568 .unwrap_err();
569 assert!(matches!(err, AuthError::MalformedCredentials(_)));
570 }
571
572 #[test]
573 fn mtls_with_subject_accepted() {
574 let m = AuthMode::Mtls;
575 let s = m
576 .validate(&AuthInput {
577 mtls_subject: Some(AuthSubject::new("CN=alice")),
578 ..Default::default()
579 })
580 .unwrap();
581 assert_eq!(s.name, "CN=alice");
582 }
583
584 #[test]
585 fn mtls_without_subject_rejected() {
586 let m = AuthMode::Mtls;
587 let err = m.validate(&AuthInput::default()).unwrap_err();
588 assert!(matches!(err, AuthError::Rejected(_)));
589 }
590
591 #[test]
592 fn sasl_plain_valid_pair_accepted() {
593 let mut users = HashMap::new();
594 users.insert("alice".to_string(), "wonderland".to_string());
595 let m = AuthMode::SaslPlain { users };
596 let blob = b"\0alice\0wonderland";
597 let s = m
598 .validate(&AuthInput {
599 sasl_plain_blob: Some(blob),
600 ..Default::default()
601 })
602 .unwrap();
603 assert_eq!(s.name, "alice");
604 }
605
606 #[test]
607 fn sasl_plain_wrong_password_rejected() {
608 let mut users = HashMap::new();
609 users.insert("alice".to_string(), "wonderland".to_string());
610 let m = AuthMode::SaslPlain { users };
611 let blob = b"\0alice\0wrong";
612 let err = m
613 .validate(&AuthInput {
614 sasl_plain_blob: Some(blob),
615 ..Default::default()
616 })
617 .unwrap_err();
618 assert!(matches!(err, AuthError::Rejected(_)));
619 }
620
621 #[test]
622 fn json_field_extracts_string() {
623 let s = r#"{"alg":"RS256","typ":"JWT"}"#;
624 assert_eq!(json_field(s, "alg").as_deref(), Some("RS256"));
625 }
626
627 #[test]
628 fn json_array_extracts_groups() {
629 let s = r#"{"sub":"a","groups":["eng","ops"]}"#;
630 let g = json_array(s, "groups").unwrap();
631 assert_eq!(g, vec!["eng".to_string(), "ops".to_string()]);
632 }
633
634 #[test]
635 fn jwt_invalid_signature_rejected() {
636 let m = AuthMode::Jwt {
641 pkcs1_pubkey_der: vec![0u8; 32],
642 expected_issuer: None,
643 expected_audience: None,
644 required_claim: None,
645 };
646 let token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhIn0.AAAA";
649 let err = m
650 .validate(&AuthInput {
651 authorization_header: Some(&format!("Bearer {token}")),
652 ..Default::default()
653 })
654 .unwrap_err();
655 assert!(matches!(err, AuthError::Rejected(_)));
656 }
657
658 #[test]
659 fn jwt_wrong_alg_rejected() {
660 let m = AuthMode::Jwt {
661 pkcs1_pubkey_der: vec![0u8; 32],
662 expected_issuer: None,
663 expected_audience: None,
664 required_claim: None,
665 };
666 let token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhIn0.AAAA";
668 let err = m
669 .validate(&AuthInput {
670 authorization_header: Some(&format!("Bearer {token}")),
671 ..Default::default()
672 })
673 .unwrap_err();
674 assert!(matches!(err, AuthError::Rejected(_)));
675 }
676
677 fn rsa_key() -> (Vec<u8>, String) {
680 use std::process::Command;
681 static SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
682 let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
683 let dir = std::env::temp_dir().join(format!("zd-jwt-{}-{seq}", std::process::id()));
684 std::fs::create_dir_all(&dir).unwrap();
685 let key = dir.join("key.pem");
686 let run = |args: &[&str]| {
687 let out = Command::new("openssl")
688 .args(args)
689 .output()
690 .expect("openssl on PATH: the JWT round trip needs a real RSA key");
691 assert!(out.status.success(), "openssl {args:?}: {out:?}");
692 out.stdout
693 };
694 run(&[
695 "genpkey",
696 "-algorithm",
697 "RSA",
698 "-pkeyopt",
699 "rsa_keygen_bits:2048",
700 "-out",
701 key.to_str().unwrap(),
702 ]);
703 let der = run(&[
704 "pkcs8",
705 "-topk8",
706 "-nocrypt",
707 "-outform",
708 "DER",
709 "-in",
710 key.to_str().unwrap(),
711 ]);
712 let spki = run(&["pkey", "-pubout", "-in", key.to_str().unwrap()]);
713 let _ = std::fs::remove_dir_all(&dir);
714 (der, String::from_utf8(spki).unwrap())
715 }
716
717 fn sign(pkcs8: &[u8], payload: &str) -> String {
718 use crate::backend::{rand, signature};
719 use base64::Engine as _;
720 let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
721 let signed = format!(
722 "{}.{}",
723 b64(br#"{"alg":"RS256","typ":"JWT"}"#),
724 b64(payload.as_bytes())
725 );
726 let pair = signature::RsaKeyPair::from_pkcs8(pkcs8).unwrap();
727 let mut sig = vec![0u8; 256]; pair.sign(
729 &signature::RSA_PKCS1_SHA256,
730 &rand::SystemRandom::new(),
731 signed.as_bytes(),
732 &mut sig,
733 )
734 .unwrap();
735 format!("{signed}.{}", b64(&sig))
736 }
737
738 #[test]
741 fn a_signed_jwt_is_checked_for_time_audience_and_claim() {
742 let (pkcs8, spki_pem) = rsa_key();
743 let mode = AuthMode::Jwt {
744 pkcs1_pubkey_der: jwt_public_key_from_pem(&spki_pem).unwrap(),
745 expected_issuer: Some("zerodds-test".into()),
746 expected_audience: Some("zerodds-grpc".into()),
747 required_claim: Some(("scope".into(), "dds.read".into())),
748 };
749 let now = unix_now();
750 let check = |payload: String| {
751 mode.validate(&AuthInput {
752 authorization_header: Some(&format!("Bearer {}", sign(&pkcs8, &payload))),
753 ..Default::default()
754 })
755 };
756 let token = |exp: u64, nbf: u64, aud: &str, scope: &str| {
757 format!(
758 r#"{{"sub":"alice","iss":"zerodds-test","aud":{aud},"scope":"{scope}","exp":{exp},"nbf":{nbf}}}"#
759 )
760 };
761 let ok = check(token(
762 now + 600,
763 now - 10,
764 r#"["x","zerodds-grpc"]"#,
765 "dds.write dds.read",
766 ));
767 assert_eq!(ok.unwrap().name, "alice");
768 let late = now - JWT_CLOCK_LEEWAY_SECS - 1;
769 for (why, payload) in [
770 (
771 "expired",
772 token(late, now - 900, r#""zerodds-grpc""#, "dds.read"),
773 ),
774 (
775 "not yet valid",
776 token(
777 now + 900,
778 now + JWT_CLOCK_LEEWAY_SECS + 60,
779 r#""zerodds-grpc""#,
780 "dds.read",
781 ),
782 ),
783 ("aud", token(now + 600, now, r#""other""#, "dds.read")),
784 (
785 "claim",
786 token(now + 600, now, r#""zerodds-grpc""#, "dds.write"),
787 ),
788 ] {
789 let err = check(payload).unwrap_err();
790 assert!(matches!(err, AuthError::Rejected(_)), "{why}: {err:?}");
791 }
792 let (other, _) = rsa_key();
794 let forged = format!(
795 "Bearer {}",
796 sign(
797 &other,
798 &token(now + 600, now, r#""zerodds-grpc""#, "dds.read")
799 )
800 );
801 assert!(
802 mode.validate(&AuthInput {
803 authorization_header: Some(&forged),
804 ..Default::default()
805 })
806 .is_err()
807 );
808 }
809
810 #[test]
811 fn a_pkcs1_pem_and_an_spki_pem_yield_the_same_key() {
812 let (_, spki_pem) = rsa_key();
813 let from_spki = jwt_public_key_from_pem(&spki_pem).unwrap();
814 use base64::Engine as _;
815 let pkcs1_pem = format!(
816 "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n",
817 base64::engine::general_purpose::STANDARD.encode(&from_spki)
818 );
819 assert_eq!(jwt_public_key_from_pem(&pkcs1_pem).unwrap(), from_spki);
820 assert!(jwt_public_key_from_pem("no key here").is_err());
821 }
822}