1use crate::claims::{AuthContext, SessionClaims};
2use crate::error::VerifyError;
3use crate::keys::{SigningKey, VerifyingKey};
4use crate::sealed::{seal_claims, unseal_claims, ClaimSealKey, ClaimSealKeys, ACCOUNT_CLAIMS};
5use base64::Engine;
6use serde::{Deserialize, Serialize};
7use serde_json;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11struct JwtHeader {
12 alg: String,
13 typ: String,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 kid: Option<String>,
16}
17
18impl Default for JwtHeader {
19 fn default() -> Self {
20 Self {
21 alg: "EdDSA".to_string(),
22 typ: "JWT".to_string(),
23 kid: None,
24 }
25 }
26}
27
28pub struct TokenSigner {
30 signing_key: SigningKey,
31 issuer: String,
32}
33
34impl TokenSigner {
35 pub fn new(signing_key: SigningKey, issuer: impl Into<String>) -> Self {
40 Self {
41 signing_key,
42 issuer: issuer.into(),
43 }
44 }
45
46 pub fn sign(&self, claims: SessionClaims) -> Result<String, TokenError> {
48 self.sign_value(&serde_json::to_value(&claims)?)
49 }
50
51 pub fn sign_sealed(
55 &self,
56 claims: SessionClaims,
57 key: &ClaimSealKey,
58 ) -> Result<String, TokenError> {
59 let mut claims = serde_json::to_value(&claims)?;
60 let object = claims
61 .as_object_mut()
62 .ok_or_else(|| TokenError::InvalidFormat("claims must be an object".to_string()))?;
63 seal_claims(object, ACCOUNT_CLAIMS, key)
64 .map_err(|error| TokenError::InvalidFormat(error.to_string()))?;
65 self.sign_value(&claims)
66 }
67
68 fn sign_value(&self, claims: &serde_json::Value) -> Result<String, TokenError> {
69 let header = JwtHeader {
71 kid: Some(self.signing_key.key_id()),
72 ..Default::default()
73 };
74
75 let header_json = serde_json::to_string(&header)?;
77 let header_b64 =
78 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header_json.as_bytes());
79
80 let claims_json = serde_json::to_string(claims)?;
82 let claims_b64 =
83 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims_json.as_bytes());
84
85 let message = format!("{}.{}", header_b64, claims_b64);
87
88 let signature = self.signing_key.sign(message.as_bytes());
90 let signature_b64 =
91 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes());
92
93 Ok(format!("{}.{}.{}", header_b64, claims_b64, signature_b64))
95 }
96
97 pub fn issuer(&self) -> &str {
99 &self.issuer
100 }
101}
102
103#[derive(Debug)]
105pub enum TokenError {
106 Serialization(serde_json::Error),
107 Base64(base64::DecodeError),
108 InvalidFormat(String),
109}
110
111impl std::fmt::Display for TokenError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 TokenError::Serialization(e) => write!(f, "Serialization error: {}", e),
115 TokenError::Base64(e) => write!(f, "Base64 error: {}", e),
116 TokenError::InvalidFormat(s) => write!(f, "Invalid format: {}", s),
117 }
118 }
119}
120
121impl std::error::Error for TokenError {}
122
123impl From<serde_json::Error> for TokenError {
124 fn from(e: serde_json::Error) -> Self {
125 TokenError::Serialization(e)
126 }
127}
128
129impl From<base64::DecodeError> for TokenError {
130 fn from(e: base64::DecodeError) -> Self {
131 TokenError::Base64(e)
132 }
133}
134
135pub struct TokenVerifier {
137 verifying_key: VerifyingKey,
138 issuer: String,
139 audiences: crate::AudienceSet,
140 require_origin: bool,
141 require_client_ip: bool,
142 claim_seal_keys: Option<ClaimSealKeys>,
143}
144
145impl TokenVerifier {
146 pub fn new(
151 verifying_key: VerifyingKey,
152 issuer: impl Into<String>,
153 audience: impl Into<String>,
154 ) -> Self {
155 Self {
156 verifying_key,
157 issuer: issuer.into(),
158 audiences: crate::AudienceSet::single(audience),
159 require_origin: false,
160 require_client_ip: false,
161 claim_seal_keys: None,
162 }
163 }
164
165 pub fn with_audiences<I, S>(
173 verifying_key: VerifyingKey,
174 issuer: impl Into<String>,
175 audiences: I,
176 ) -> Result<Self, crate::AudienceSetError>
177 where
178 I: IntoIterator<Item = S>,
179 S: Into<String>,
180 {
181 Ok(Self {
182 verifying_key,
183 issuer: issuer.into(),
184 audiences: crate::AudienceSet::new(audiences)?,
185 require_origin: false,
186 require_client_ip: false,
187 claim_seal_keys: None,
188 })
189 }
190
191 pub fn with_origin_validation(mut self) -> Self {
193 self.require_origin = true;
194 self
195 }
196
197 pub fn with_client_ip_validation(mut self) -> Self {
203 self.require_client_ip = true;
204 self
205 }
206
207 pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
210 self.claim_seal_keys = Some(keys);
211 self
212 }
213
214 pub fn verify(
221 &self,
222 token: &str,
223 expected_origin: Option<&str>,
224 expected_client_ip: Option<&str>,
225 ) -> Result<AuthContext, VerifyError> {
226 let parts: Vec<&str> = token.split('.').collect();
228 if parts.len() != 3 {
229 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
230 }
231
232 let header_b64 = parts[0];
233 let claims_b64 = parts[1];
234 let signature_b64 = parts[2];
235
236 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
238 .decode(header_b64)
239 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header base64: {}", e)))?;
240 let header: JwtHeader = serde_json::from_slice(&header_json)
241 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
242
243 if header.alg != "EdDSA" {
244 return Err(VerifyError::InvalidFormat(format!(
245 "Unsupported algorithm: {}",
246 header.alg
247 )));
248 }
249
250 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
252 .decode(claims_b64)
253 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims base64: {}", e)))?;
254
255 let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
257 .decode(signature_b64)
258 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid signature base64: {}", e)))?;
259 if signature_bytes.len() != 64 {
260 return Err(VerifyError::InvalidFormat(
261 "Invalid signature length".to_string(),
262 ));
263 }
264 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes.try_into().unwrap());
265
266 let message = format!("{}.{}", header_b64, claims_b64);
268 self.verifying_key
269 .verify(message.as_bytes(), &signature)
270 .map_err(|_| VerifyError::InvalidSignature)?;
271
272 let claims = decode_claims(&claims_json, self.claim_seal_keys.as_ref())?;
274
275 if claims.iss != self.issuer {
277 return Err(VerifyError::InvalidIssuer);
278 }
279
280 if !self.audiences.accepts(&claims.aud) {
283 return Err(VerifyError::InvalidAudience);
284 }
285
286 use std::time::{SystemTime, UNIX_EPOCH};
288 let now = SystemTime::now()
289 .duration_since(UNIX_EPOCH)
290 .expect("time should not be before epoch")
291 .as_secs();
292
293 if claims.exp <= now {
294 return Err(VerifyError::Expired);
295 }
296
297 if claims.nbf > now || claims.iat > now {
298 return Err(VerifyError::NotYetValid);
299 }
300
301 let token_has_origin = claims.origin.is_some();
303 let origin_provided = expected_origin.is_some();
304
305 if token_has_origin && origin_provided {
306 let expected = expected_origin.unwrap();
308 let actual = claims.origin.as_ref().unwrap();
309
310 if actual != expected {
311 return Err(VerifyError::OriginMismatch {
312 expected: expected.to_string(),
313 actual: actual.clone(),
314 });
315 }
316 } else if token_has_origin && self.require_origin {
317 return Err(VerifyError::OriginRequired {
319 token_origin: claims.origin.as_ref().unwrap().clone(),
320 });
321 } else if !token_has_origin && self.require_origin {
322 return Err(VerifyError::MissingClaim("origin".to_string()));
324 }
325 if self.require_client_ip {
331 let actual = claims
332 .client_ip
333 .as_ref()
334 .ok_or_else(|| VerifyError::MissingClaim("client_ip".to_string()))?;
335 let expected = expected_client_ip
336 .ok_or_else(|| VerifyError::MissingClaim("client_ip".to_string()))?;
337 if actual != expected {
338 return Err(VerifyError::OriginMismatch {
339 expected: expected.to_string(),
340 actual: actual.clone(),
341 });
342 }
343 }
344
345 claims
348 .validate_policy_claims()
349 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
350
351 Ok(AuthContext::from_claims(claims))
352 }
353
354 pub fn issuer(&self) -> &str {
356 &self.issuer
357 }
358
359 pub fn audiences(&self) -> &crate::AudienceSet {
361 &self.audiences
362 }
363
364 #[deprecated(
373 since = "0.5.0",
374 note = "use `audiences()`; a verifier may accept more than one audience"
375 )]
376 pub fn audience(&self) -> &str {
377 self.audiences.iter().next().unwrap_or_default()
378 }
379}
380
381pub(crate) fn decode_claims(
383 claims_json: &[u8],
384 keys: Option<&ClaimSealKeys>,
385) -> Result<SessionClaims, VerifyError> {
386 let mut claims: serde_json::Value = serde_json::from_slice(claims_json)
387 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
388 let object = claims
389 .as_object_mut()
390 .ok_or_else(|| VerifyError::InvalidFormat("Invalid claims JSON: not an object".into()))?;
391 unseal_claims(object, keys)?;
392 serde_json::from_value(claims)
393 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))
394}
395
396#[derive(Debug, Clone, Deserialize)]
398pub struct Jwks {
399 pub keys: Vec<Jwk>,
400}
401
402#[derive(Debug, Clone, Deserialize)]
403pub struct Jwk {
404 pub kty: String,
405 #[serde(rename = "use")]
406 pub use_: Option<String>,
407 pub kid: String,
408 pub x: String, }
410
411#[derive(Clone)]
413pub struct JwksVerifier {
414 jwks: Jwks,
415 issuer: String,
416 audiences: crate::AudienceSet,
417 require_origin: bool,
418 claim_seal_keys: Option<ClaimSealKeys>,
419}
420
421impl JwksVerifier {
422 pub(crate) fn with_audience_set(
424 jwks: Jwks,
425 issuer: impl Into<String>,
426 audiences: crate::AudienceSet,
427 ) -> Self {
428 Self {
429 jwks,
430 issuer: issuer.into(),
431 audiences,
432 require_origin: false,
433 claim_seal_keys: None,
434 }
435 }
436
437 pub fn new(jwks: Jwks, issuer: impl Into<String>, audience: impl Into<String>) -> Self {
439 Self {
440 jwks,
441 issuer: issuer.into(),
442 audiences: crate::AudienceSet::single(audience),
443 require_origin: false,
444 claim_seal_keys: None,
445 }
446 }
447
448 pub fn with_origin_validation(mut self) -> Self {
450 self.require_origin = true;
451 self
452 }
453
454 pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
456 self.claim_seal_keys = Some(keys);
457 self
458 }
459
460 pub fn verify(
462 &self,
463 token: &str,
464 expected_origin: Option<&str>,
465 expected_client_ip: Option<&str>,
466 ) -> Result<AuthContext, VerifyError> {
467 let parts: Vec<&str> = token.split('.').collect();
469 if parts.len() != 3 {
470 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
471 }
472
473 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
474 .decode(parts[0])
475 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header: {}", e)))?;
476 let header: JwtHeader = serde_json::from_slice(&header_json)
477 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
478
479 let kid = header
480 .kid
481 .ok_or_else(|| VerifyError::MissingClaim("kid".to_string()))?;
482
483 let jwk = self
485 .jwks
486 .keys
487 .iter()
488 .find(|k| k.kid == kid)
489 .ok_or(VerifyError::KeyNotFound(kid))?;
490
491 let public_key_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
495 .decode(&jwk.x)
496 .map_err(|_| VerifyError::InvalidFormat("Invalid public key base64".to_string()))?;
497
498 let public_key: [u8; 32] = public_key_bytes
499 .try_into()
500 .map_err(|_| VerifyError::InvalidFormat("Invalid key length".to_string()))?;
501
502 let verifying_key = VerifyingKey::from_bytes(&public_key)
504 .map_err(|e| VerifyError::InvalidFormat(e.to_string()))?;
505
506 let verifier = TokenVerifier {
507 verifying_key,
508 issuer: self.issuer.clone(),
509 audiences: self.audiences.clone(),
510 require_origin: self.require_origin,
511 require_client_ip: false,
512 claim_seal_keys: self.claim_seal_keys.clone(),
513 };
514
515 verifier.verify(token, expected_origin, expected_client_ip)
516 }
517
518 #[cfg(feature = "jwks")]
520 pub async fn fetch_jwks(url: &str) -> Result<Jwks, reqwest::Error> {
521 let response = reqwest::get(url).await?;
522 let jwks: Jwks = response.json().await?;
523 Ok(jwks)
524 }
525}
526
527#[cfg(test)]
528pub struct HmacVerifier {
530 _secret: Vec<u8>,
531 _issuer: String,
532 _audience: String,
533}
534
535#[cfg(test)]
536impl HmacVerifier {
537 pub fn new(
539 secret: impl Into<Vec<u8>>,
540 issuer: impl Into<String>,
541 audience: impl Into<String>,
542 ) -> Self {
543 Self {
544 _secret: secret.into(),
545 _issuer: issuer.into(),
546 _audience: audience.into(),
547 }
548 }
549
550 pub fn verify(
552 &self,
553 token: &str,
554 _expected_origin: Option<&str>,
555 ) -> Result<AuthContext, VerifyError> {
556 let parts: Vec<&str> = token.split('.').collect();
558 if parts.len() != 3 {
559 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
560 }
561
562 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
566 .decode(parts[1])
567 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims: {}", e)))?;
568 let claims: SessionClaims = serde_json::from_slice(&claims_json)
569 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
570
571 claims
572 .validate_policy_claims()
573 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
574
575 Ok(AuthContext::from_claims(claims))
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::claims::{KeyClass, Limits};
583
584 fn create_test_claims() -> SessionClaims {
585 SessionClaims::builder("test-issuer", "test-subject", "test-audience")
586 .with_ttl(300)
587 .with_scope("read")
588 .with_metering_key("meter-123")
589 .with_key_class(KeyClass::Publishable)
590 .with_limits(Limits {
591 max_connections: Some(10),
592 max_subscriptions: Some(100),
593 max_snapshot_rows: Some(1000),
594 max_messages_per_minute: Some(1000),
595 max_bytes_per_minute: Some(10_000_000),
596 max_http_requests_per_minute: Some(300),
597 max_http_batch_addresses: Some(100),
598 ..Limits::default()
599 })
600 .build()
601 }
602
603 #[test]
604 fn test_sign_and_verify() {
605 let signing_key = crate::keys::SigningKey::generate();
607 let verifying_key = signing_key.verifying_key();
608
609 let signer = TokenSigner::new(signing_key, "test-issuer");
611 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
612
613 let claims = create_test_claims();
615 let token = signer.sign(claims.clone()).unwrap();
616
617 let context = verifier.verify(&token, None, None).unwrap();
619
620 assert_eq!(context.subject, "test-subject");
621 assert_eq!(context.issuer, "test-issuer");
622 assert_eq!(context.metering_key, "meter-123");
623 }
624
625 fn account_claims() -> SessionClaims {
626 SessionClaims::builder("test-issuer", "anon:visitor", "test-audience")
627 .with_ttl(300)
628 .with_scope("read")
629 .with_metering_key("account:42")
630 .with_key_class(KeyClass::Publishable)
631 .with_plan("pro")
632 .with_limits(Limits {
633 max_connections: Some(10),
634 ..Limits::default()
635 })
636 .with_actor_key("user:7")
637 .with_account_key("account:42")
638 .with_consumer_key("consumer:abc")
639 .with_policy_version(4)
640 .with_account_limits(Limits {
641 max_connections: Some(500),
642 ..Limits::default()
643 })
644 .build()
645 }
646
647 fn decoded_payload(token: &str) -> serde_json::Value {
648 let payload = token.split('.').nth(1).unwrap();
649 serde_json::from_slice(
650 &base64::engine::general_purpose::URL_SAFE_NO_PAD
651 .decode(payload)
652 .unwrap(),
653 )
654 .unwrap()
655 }
656
657 #[test]
658 fn sealed_tokens_hide_account_claims_and_verify_with_the_key() {
659 let signing_key = crate::keys::SigningKey::generate();
660 let verifying_key = signing_key.verifying_key();
661 let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
662 let signer = TokenSigner::new(signing_key, "test-issuer");
663 let token = signer.sign_sealed(account_claims(), &seal).unwrap();
664
665 let payload = decoded_payload(&token);
666 for name in ACCOUNT_CLAIMS {
667 assert!(payload.get(*name).is_none(), "{name} visible in {payload}");
668 }
669 let mut clear = payload.as_object().unwrap().clone();
670 clear.remove(crate::sealed::SEALED_CLAIM);
671 let text = serde_json::to_string(&clear).unwrap();
672 for secret in ["account:42", "user:7", "pro", "500"] {
673 assert!(!text.contains(secret), "{secret} visible in {text}");
674 }
675 assert_eq!(payload["sub"], "anon:visitor");
676
677 let context = TokenVerifier::new(verifying_key.clone(), "test-issuer", "test-audience")
678 .with_claim_seal_keys(ClaimSealKeys::new(seal))
679 .verify(&token, None, None)
680 .unwrap();
681 assert_eq!(context.metering_key, "account:42");
682 assert_eq!(context.account_key(), "account:42");
683 assert_eq!(context.plan.as_deref(), Some("pro"));
684 assert_eq!(context.limits.max_connections, Some(10));
685 assert_eq!(context.account_limits.max_connections, Some(500));
686 assert_eq!(context.policy_version, Some(4));
687
688 let unkeyed = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
689 .verify(&token, None, None)
690 .unwrap_err();
691 assert_eq!(
692 unkeyed,
693 VerifyError::SealedClaims(crate::sealed::SealError::KeyRequired)
694 );
695 }
696
697 #[test]
698 fn sealed_claims_are_opened_only_after_the_signature_checks() {
699 let signing_key = crate::keys::SigningKey::generate();
700 let other = crate::keys::SigningKey::generate();
701 let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
702 let token = TokenSigner::new(other, "test-issuer")
703 .sign_sealed(account_claims(), &seal)
704 .unwrap();
705 let error = TokenVerifier::new(signing_key.verifying_key(), "test-issuer", "test-audience")
706 .with_claim_seal_keys(ClaimSealKeys::new(seal))
707 .verify(&token, None, None)
708 .unwrap_err();
709 assert_eq!(error, VerifyError::InvalidSignature);
710 }
711
712 #[test]
713 fn legacy_claims_without_typed_target_deserialize_and_verify() {
714 use std::time::{SystemTime, UNIX_EPOCH};
715
716 let now = SystemTime::now()
717 .duration_since(UNIX_EPOCH)
718 .unwrap()
719 .as_secs();
720 let claims: SessionClaims = serde_json::from_value(serde_json::json!({
721 "iss": "test-issuer",
722 "sub": "legacy-subject",
723 "aud": "deployment-1",
724 "iat": now,
725 "nbf": now,
726 "exp": now + 300,
727 "jti": "legacy-jti",
728 "scope": "read",
729 "metering_key": "api_key:1",
730 "deployment_id": "deployment-1",
731 "key_class": "publishable"
732 }))
733 .unwrap();
734 assert_eq!(claims.target_kind, None);
735 assert_eq!(claims.target_id, None);
736 assert_eq!(claims.program_id, None);
737 assert_eq!(claims.program_release_hash, None);
738
739 let signing_key = crate::keys::SigningKey::generate();
740 let verifying_key = signing_key.verifying_key();
741 let token = TokenSigner::new(signing_key, "test-issuer")
742 .sign(claims)
743 .unwrap();
744 let context = TokenVerifier::new(verifying_key, "test-issuer", "deployment-1")
745 .verify(&token, None, None)
746 .unwrap();
747
748 assert_eq!(context.subject, "legacy-subject");
749 assert_eq!(context.deployment_id.as_deref(), Some("deployment-1"));
750 assert_eq!(context.target_kind, None);
751 }
752
753 #[test]
754 fn verifier_rejects_partial_v2_policy_claims_and_accepts_full_sets() {
755 let signing_key = crate::keys::SigningKey::generate();
756 let verifying_key = signing_key.verifying_key();
757 let signer = TokenSigner::new(signing_key, "test-issuer");
758 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
759
760 let partial = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
763 .with_metering_key("account:42")
764 .with_account_key("account:42")
765 .build();
766 let token = signer.sign(partial).unwrap();
767 assert!(matches!(
768 verifier.verify(&token, None, None),
769 Err(VerifyError::InvalidPolicyClaims(_))
770 ));
771
772 let full = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
774 .with_metering_key("account:42")
775 .with_plan("pro")
776 .with_actor_key("user:1")
777 .with_account_key("account:42")
778 .with_consumer_key("consumer:abc123")
779 .with_policy_version(2)
780 .with_account_limits(Limits::default())
781 .build();
782 let token = signer.sign(full).unwrap();
783 let context = verifier.verify(&token, None, None).unwrap();
784 assert!(!context.is_legacy_policy());
785 assert_eq!(context.account_key(), "account:42");
786 assert_eq!(context.consumer_key(), "consumer:abc123");
787 assert_eq!(context.policy_version, Some(2));
788 }
789
790 #[test]
791 fn test_expired_token() {
792 let signing_key = crate::keys::SigningKey::generate();
793 let verifying_key = signing_key.verifying_key();
794
795 let signer = TokenSigner::new(signing_key, "test-issuer");
796 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
797
798 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
800 .with_ttl(0) .with_scope("read")
802 .with_metering_key("meter-123")
803 .with_key_class(KeyClass::Publishable)
804 .build();
805
806 let token = signer.sign(claims).unwrap();
807
808 let result = verifier.verify(&token, None, None);
810 assert!(matches!(result, Err(VerifyError::Expired)));
811 }
812
813 #[test]
814 fn test_future_issued_token_is_not_yet_valid() {
815 let signing_key = crate::keys::SigningKey::generate();
816 let verifying_key = signing_key.verifying_key();
817 let signer = TokenSigner::new(signing_key, "test-issuer");
818 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
819 let mut claims = create_test_claims();
820 claims.iat += 300;
821
822 let token = signer.sign(claims).unwrap();
823
824 assert!(matches!(
825 verifier.verify(&token, None, None),
826 Err(VerifyError::NotYetValid)
827 ));
828 }
829
830 #[test]
831 fn test_invalid_signature() {
832 let signing_key = crate::keys::SigningKey::generate();
833 let wrong_signing_key = crate::keys::SigningKey::generate();
834 let wrong_verifying_key = wrong_signing_key.verifying_key();
835
836 let signer = TokenSigner::new(signing_key, "test-issuer");
837 let verifier = TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
838
839 let claims = create_test_claims();
840 let token = signer.sign(claims).unwrap();
841
842 let result = verifier.verify(&token, None, None);
844 assert!(matches!(result, Err(VerifyError::InvalidSignature)));
845 }
846
847 #[test]
848 fn test_wrong_issuer() {
849 let signing_key = crate::keys::SigningKey::generate();
850 let verifying_key = signing_key.verifying_key();
851
852 let signer = TokenSigner::new(signing_key, "wrong-issuer");
853 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
854
855 let claims = SessionClaims::builder("wrong-issuer", "test-subject", "test-audience")
857 .with_ttl(300)
858 .with_scope("read")
859 .with_metering_key("meter-123")
860 .with_key_class(KeyClass::Publishable)
861 .build();
862 let token = signer.sign(claims).unwrap();
863
864 let result = verifier.verify(&token, None, None);
866 assert!(matches!(result, Err(VerifyError::InvalidIssuer)));
867 }
868
869 #[test]
870 #[allow(deprecated)]
871 fn the_single_audience_accessor_still_answers_for_scalar_verifiers() {
872 let verifying_key = crate::keys::SigningKey::generate().verifying_key();
874 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "deployment-31");
875 assert_eq!(verifier.audience(), "deployment-31");
876 assert_eq!(verifier.audiences().as_single(), Some("deployment-31"));
877 }
878
879 #[test]
882 fn a_multi_audience_verifier_accepts_each_and_reports_which_matched() {
883 let signing_key = crate::keys::SigningKey::generate();
884 let verifying_key = signing_key.verifying_key();
885 let signer = TokenSigner::new(signing_key, "test-issuer");
886
887 let verifier = TokenVerifier::with_audiences(
888 verifying_key,
889 "test-issuer",
890 ["deployment-31", "deployment-32"],
891 )
892 .expect("non-empty audience set");
893
894 for audience in ["deployment-31", "deployment-32"] {
895 let claims = SessionClaims::builder("test-issuer", "test-subject", audience)
896 .with_ttl(300)
897 .with_scope("read")
898 .with_metering_key("meter-123")
899 .with_key_class(KeyClass::Publishable)
900 .build();
901 let token = signer.sign(claims).unwrap();
902
903 let ctx = verifier
904 .verify(&token, None, None)
905 .unwrap_or_else(|error| panic!("{audience} should verify: {error:?}"));
906 assert_eq!(ctx.audience, audience);
908 }
909
910 let claims = SessionClaims::builder("test-issuer", "test-subject", "deployment-99")
913 .with_ttl(300)
914 .with_scope("read")
915 .with_metering_key("meter-123")
916 .with_key_class(KeyClass::Publishable)
917 .build();
918 let token = signer.sign(claims).unwrap();
919 assert!(matches!(
920 verifier.verify(&token, None, None),
921 Err(VerifyError::InvalidAudience)
922 ));
923 }
924
925 #[test]
926 fn test_wrong_audience() {
927 let signing_key = crate::keys::SigningKey::generate();
928 let verifying_key = signing_key.verifying_key();
929
930 let signer = TokenSigner::new(signing_key, "test-issuer");
931 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "expected-audience");
932
933 let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
934 .with_ttl(300)
935 .with_scope("read")
936 .with_metering_key("meter-123")
937 .with_key_class(KeyClass::Publishable)
938 .build();
939 let token = signer.sign(claims).unwrap();
940
941 let result = verifier.verify(&token, None, None);
942 assert!(matches!(result, Err(VerifyError::InvalidAudience)));
943 }
944
945 #[test]
946 fn test_origin_mismatch() {
947 let signing_key = crate::keys::SigningKey::generate();
948 let verifying_key = signing_key.verifying_key();
949
950 let signer = TokenSigner::new(signing_key, "test-issuer");
951 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
952 .with_origin_validation();
953
954 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
955 .with_ttl(300)
956 .with_scope("read")
957 .with_metering_key("meter-123")
958 .with_origin("https://allowed.example")
959 .with_key_class(KeyClass::Publishable)
960 .build();
961 let token = signer.sign(claims).unwrap();
962
963 let result = verifier.verify(&token, Some("https://other.example"), None);
964 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
965 }
966
967 #[test]
968 fn test_origin_validation_success() {
969 let signing_key = crate::keys::SigningKey::generate();
970 let verifying_key = signing_key.verifying_key();
971
972 let signer = TokenSigner::new(signing_key, "test-issuer");
973 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
974 .with_origin_validation();
975
976 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
977 .with_ttl(300)
978 .with_scope("read")
979 .with_metering_key("meter-123")
980 .with_origin("https://allowed.example")
981 .with_key_class(KeyClass::Publishable)
982 .build();
983 let token = signer.sign(claims).unwrap();
984
985 let context = verifier
986 .verify(&token, Some("https://allowed.example"), None)
987 .unwrap();
988 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
989 }
990
991 #[test]
992 fn test_origin_validation_requires_origin_claim() {
993 let signing_key = crate::keys::SigningKey::generate();
994 let verifying_key = signing_key.verifying_key();
995
996 let signer = TokenSigner::new(signing_key, "test-issuer");
997 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
998 .with_origin_validation();
999
1000 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1001 .with_ttl(300)
1002 .with_scope("read")
1003 .with_metering_key("meter-123")
1004 .with_key_class(KeyClass::Publishable)
1005 .build();
1006 let token = signer.sign(claims).unwrap();
1007
1008 let result = verifier.verify(&token, None, None);
1009 assert!(matches!(
1010 result,
1011 Err(VerifyError::MissingClaim(ref claim)) if claim == "origin"
1012 ));
1013 }
1014
1015 #[test]
1016 fn test_client_ip_validation_requires_client_ip_claim() {
1017 let signing_key = crate::keys::SigningKey::generate();
1018 let verifying_key = signing_key.verifying_key();
1019
1020 let signer = TokenSigner::new(signing_key, "test-issuer");
1021 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1022 .with_client_ip_validation();
1023
1024 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1025 .with_ttl(300)
1026 .with_scope("read")
1027 .with_metering_key("meter-123")
1028 .with_key_class(KeyClass::Publishable)
1029 .build();
1030 let token = signer.sign(claims).unwrap();
1031
1032 let result = verifier.verify(&token, None, None);
1033 assert!(matches!(
1034 result,
1035 Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
1036 ));
1037 }
1038
1039 fn ip_bound_token(signer: &TokenSigner, client_ip: &str) -> String {
1040 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1041 .with_ttl(300)
1042 .with_scope("read")
1043 .with_metering_key("meter-123")
1044 .with_key_class(KeyClass::Publishable)
1045 .with_client_ip(client_ip)
1046 .build();
1047 signer.sign(claims).unwrap()
1048 }
1049
1050 #[test]
1051 fn test_client_ip_validation_fails_closed_when_client_ip_unknown() {
1052 let signing_key = crate::keys::SigningKey::generate();
1053 let verifying_key = signing_key.verifying_key();
1054 let signer = TokenSigner::new(signing_key, "test-issuer");
1055 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1056 .with_client_ip_validation();
1057 let token = ip_bound_token(&signer, "198.51.100.7");
1058
1059 assert!(matches!(
1060 verifier.verify(&token, None, None),
1061 Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
1062 ));
1063 assert!(matches!(
1064 verifier.verify(&token, None, Some("203.0.113.9")),
1065 Err(VerifyError::OriginMismatch { .. })
1066 ));
1067 let context = verifier.verify(&token, None, Some("198.51.100.7")).unwrap();
1068 assert_eq!(context.subject, "test-subject");
1069 }
1070
1071 #[test]
1072 fn test_ip_bound_token_passes_when_client_ip_validation_is_off() {
1073 let signing_key = crate::keys::SigningKey::generate();
1074 let verifying_key = signing_key.verifying_key();
1075 let signer = TokenSigner::new(signing_key, "test-issuer");
1076 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1077 let token = ip_bound_token(&signer, "198.51.100.7");
1078
1079 assert!(verifier.verify(&token, None, None).is_ok());
1080 }
1081
1082 #[test]
1083 fn test_origin_bound_token_allows_no_origin_when_not_required() {
1084 let signing_key = crate::keys::SigningKey::generate();
1090 let verifying_key = signing_key.verifying_key();
1091
1092 let signer = TokenSigner::new(signing_key, "test-issuer");
1093 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1095
1096 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1097 .with_ttl(300)
1098 .with_scope("read")
1099 .with_metering_key("meter-123")
1100 .with_origin("https://example.com") .with_key_class(KeyClass::Publishable)
1102 .build();
1103 let token = signer.sign(claims).unwrap();
1104
1105 let context = verifier.verify(&token, None, None).unwrap();
1107 assert_eq!(context.origin.as_deref(), Some("https://example.com"));
1108 }
1109
1110 #[test]
1111 fn test_origin_bound_token_validates_when_origin_provided_even_when_not_required() {
1112 let signing_key = crate::keys::SigningKey::generate();
1115 let verifying_key = signing_key.verifying_key();
1116
1117 let signer = TokenSigner::new(signing_key, "test-issuer");
1118 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1120
1121 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1122 .with_ttl(300)
1123 .with_scope("read")
1124 .with_metering_key("meter-123")
1125 .with_origin("https://allowed.example")
1126 .with_key_class(KeyClass::Publishable)
1127 .build();
1128 let token = signer.sign(claims).unwrap();
1129
1130 let context = verifier
1132 .verify(&token, Some("https://allowed.example"), None)
1133 .unwrap();
1134 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
1135
1136 let result = verifier.verify(&token, Some("https://evil.example"), None);
1138 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
1139 }
1140}