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 {
199 self.require_client_ip = true;
200 self
201 }
202
203 pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
206 self.claim_seal_keys = Some(keys);
207 self
208 }
209
210 pub fn verify(
217 &self,
218 token: &str,
219 expected_origin: Option<&str>,
220 expected_client_ip: Option<&str>,
221 ) -> Result<AuthContext, VerifyError> {
222 let parts: Vec<&str> = token.split('.').collect();
224 if parts.len() != 3 {
225 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
226 }
227
228 let header_b64 = parts[0];
229 let claims_b64 = parts[1];
230 let signature_b64 = parts[2];
231
232 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
234 .decode(header_b64)
235 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header base64: {}", e)))?;
236 let header: JwtHeader = serde_json::from_slice(&header_json)
237 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
238
239 if header.alg != "EdDSA" {
240 return Err(VerifyError::InvalidFormat(format!(
241 "Unsupported algorithm: {}",
242 header.alg
243 )));
244 }
245
246 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
248 .decode(claims_b64)
249 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims base64: {}", e)))?;
250
251 let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
253 .decode(signature_b64)
254 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid signature base64: {}", e)))?;
255 if signature_bytes.len() != 64 {
256 return Err(VerifyError::InvalidFormat(
257 "Invalid signature length".to_string(),
258 ));
259 }
260 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes.try_into().unwrap());
261
262 let message = format!("{}.{}", header_b64, claims_b64);
264 self.verifying_key
265 .verify(message.as_bytes(), &signature)
266 .map_err(|_| VerifyError::InvalidSignature)?;
267
268 let claims = decode_claims(&claims_json, self.claim_seal_keys.as_ref())?;
270
271 if claims.iss != self.issuer {
273 return Err(VerifyError::InvalidIssuer);
274 }
275
276 if !self.audiences.accepts(&claims.aud) {
279 return Err(VerifyError::InvalidAudience);
280 }
281
282 use std::time::{SystemTime, UNIX_EPOCH};
284 let now = SystemTime::now()
285 .duration_since(UNIX_EPOCH)
286 .expect("time should not be before epoch")
287 .as_secs();
288
289 if claims.exp <= now {
290 return Err(VerifyError::Expired);
291 }
292
293 if claims.nbf > now || claims.iat > now {
294 return Err(VerifyError::NotYetValid);
295 }
296
297 let token_has_origin = claims.origin.is_some();
299 let origin_provided = expected_origin.is_some();
300
301 if token_has_origin && origin_provided {
302 let expected = expected_origin.unwrap();
304 let actual = claims.origin.as_ref().unwrap();
305
306 if actual != expected {
307 return Err(VerifyError::OriginMismatch {
308 expected: expected.to_string(),
309 actual: actual.clone(),
310 });
311 }
312 } else if token_has_origin && self.require_origin {
313 return Err(VerifyError::OriginRequired {
315 token_origin: claims.origin.as_ref().unwrap().clone(),
316 });
317 } else if !token_has_origin && self.require_origin {
318 return Err(VerifyError::MissingClaim("origin".to_string()));
320 }
321 if self.require_client_ip {
326 if let Some(expected) = expected_client_ip {
327 match &claims.client_ip {
328 Some(actual) if actual == expected => {}
329 Some(actual) => {
330 return Err(VerifyError::OriginMismatch {
331 expected: expected.to_string(),
332 actual: actual.clone(),
333 });
334 }
335 None => {
336 return Err(VerifyError::MissingClaim("client_ip".to_string()));
337 }
338 }
339 } else if claims.client_ip.is_none() {
340 return Err(VerifyError::MissingClaim("client_ip".to_string()));
341 }
342 }
343
344 claims
347 .validate_policy_claims()
348 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
349
350 Ok(AuthContext::from_claims(claims))
351 }
352
353 pub fn issuer(&self) -> &str {
355 &self.issuer
356 }
357
358 pub fn audiences(&self) -> &crate::AudienceSet {
360 &self.audiences
361 }
362
363 #[deprecated(
372 since = "0.5.0",
373 note = "use `audiences()`; a verifier may accept more than one audience"
374 )]
375 pub fn audience(&self) -> &str {
376 self.audiences.iter().next().unwrap_or_default()
377 }
378}
379
380pub(crate) fn decode_claims(
382 claims_json: &[u8],
383 keys: Option<&ClaimSealKeys>,
384) -> Result<SessionClaims, VerifyError> {
385 let mut claims: serde_json::Value = serde_json::from_slice(claims_json)
386 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
387 let object = claims
388 .as_object_mut()
389 .ok_or_else(|| VerifyError::InvalidFormat("Invalid claims JSON: not an object".into()))?;
390 unseal_claims(object, keys)?;
391 serde_json::from_value(claims)
392 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))
393}
394
395#[derive(Debug, Clone, Deserialize)]
397pub struct Jwks {
398 pub keys: Vec<Jwk>,
399}
400
401#[derive(Debug, Clone, Deserialize)]
402pub struct Jwk {
403 pub kty: String,
404 #[serde(rename = "use")]
405 pub use_: Option<String>,
406 pub kid: String,
407 pub x: String, }
409
410#[derive(Clone)]
412pub struct JwksVerifier {
413 jwks: Jwks,
414 issuer: String,
415 audiences: crate::AudienceSet,
416 require_origin: bool,
417 claim_seal_keys: Option<ClaimSealKeys>,
418}
419
420impl JwksVerifier {
421 pub(crate) fn with_audience_set(
423 jwks: Jwks,
424 issuer: impl Into<String>,
425 audiences: crate::AudienceSet,
426 ) -> Self {
427 Self {
428 jwks,
429 issuer: issuer.into(),
430 audiences,
431 require_origin: false,
432 claim_seal_keys: None,
433 }
434 }
435
436 pub fn new(jwks: Jwks, issuer: impl Into<String>, audience: impl Into<String>) -> Self {
438 Self {
439 jwks,
440 issuer: issuer.into(),
441 audiences: crate::AudienceSet::single(audience),
442 require_origin: false,
443 claim_seal_keys: None,
444 }
445 }
446
447 pub fn with_origin_validation(mut self) -> Self {
449 self.require_origin = true;
450 self
451 }
452
453 pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
455 self.claim_seal_keys = Some(keys);
456 self
457 }
458
459 pub fn verify(
461 &self,
462 token: &str,
463 expected_origin: Option<&str>,
464 expected_client_ip: Option<&str>,
465 ) -> Result<AuthContext, VerifyError> {
466 let parts: Vec<&str> = token.split('.').collect();
468 if parts.len() != 3 {
469 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
470 }
471
472 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
473 .decode(parts[0])
474 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header: {}", e)))?;
475 let header: JwtHeader = serde_json::from_slice(&header_json)
476 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
477
478 let kid = header
479 .kid
480 .ok_or_else(|| VerifyError::MissingClaim("kid".to_string()))?;
481
482 let jwk = self
484 .jwks
485 .keys
486 .iter()
487 .find(|k| k.kid == kid)
488 .ok_or(VerifyError::KeyNotFound(kid))?;
489
490 let public_key_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
494 .decode(&jwk.x)
495 .map_err(|_| VerifyError::InvalidFormat("Invalid public key base64".to_string()))?;
496
497 let public_key: [u8; 32] = public_key_bytes
498 .try_into()
499 .map_err(|_| VerifyError::InvalidFormat("Invalid key length".to_string()))?;
500
501 let verifying_key = VerifyingKey::from_bytes(&public_key)
503 .map_err(|e| VerifyError::InvalidFormat(e.to_string()))?;
504
505 let verifier = TokenVerifier {
506 verifying_key,
507 issuer: self.issuer.clone(),
508 audiences: self.audiences.clone(),
509 require_origin: self.require_origin,
510 require_client_ip: false,
511 claim_seal_keys: self.claim_seal_keys.clone(),
512 };
513
514 verifier.verify(token, expected_origin, expected_client_ip)
515 }
516
517 #[cfg(feature = "jwks")]
519 pub async fn fetch_jwks(url: &str) -> Result<Jwks, reqwest::Error> {
520 let response = reqwest::get(url).await?;
521 let jwks: Jwks = response.json().await?;
522 Ok(jwks)
523 }
524}
525
526#[cfg(test)]
527pub struct HmacVerifier {
529 _secret: Vec<u8>,
530 _issuer: String,
531 _audience: String,
532}
533
534#[cfg(test)]
535impl HmacVerifier {
536 pub fn new(
538 secret: impl Into<Vec<u8>>,
539 issuer: impl Into<String>,
540 audience: impl Into<String>,
541 ) -> Self {
542 Self {
543 _secret: secret.into(),
544 _issuer: issuer.into(),
545 _audience: audience.into(),
546 }
547 }
548
549 pub fn verify(
551 &self,
552 token: &str,
553 _expected_origin: Option<&str>,
554 ) -> Result<AuthContext, VerifyError> {
555 let parts: Vec<&str> = token.split('.').collect();
557 if parts.len() != 3 {
558 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
559 }
560
561 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
565 .decode(parts[1])
566 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims: {}", e)))?;
567 let claims: SessionClaims = serde_json::from_slice(&claims_json)
568 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
569
570 claims
571 .validate_policy_claims()
572 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
573
574 Ok(AuthContext::from_claims(claims))
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use crate::claims::{KeyClass, Limits};
582
583 fn create_test_claims() -> SessionClaims {
584 SessionClaims::builder("test-issuer", "test-subject", "test-audience")
585 .with_ttl(300)
586 .with_scope("read")
587 .with_metering_key("meter-123")
588 .with_key_class(KeyClass::Publishable)
589 .with_limits(Limits {
590 max_connections: Some(10),
591 max_subscriptions: Some(100),
592 max_snapshot_rows: Some(1000),
593 max_messages_per_minute: Some(1000),
594 max_bytes_per_minute: Some(10_000_000),
595 max_http_requests_per_minute: Some(300),
596 max_http_batch_addresses: Some(100),
597 ..Limits::default()
598 })
599 .build()
600 }
601
602 #[test]
603 fn test_sign_and_verify() {
604 let signing_key = crate::keys::SigningKey::generate();
606 let verifying_key = signing_key.verifying_key();
607
608 let signer = TokenSigner::new(signing_key, "test-issuer");
610 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
611
612 let claims = create_test_claims();
614 let token = signer.sign(claims.clone()).unwrap();
615
616 let context = verifier.verify(&token, None, None).unwrap();
618
619 assert_eq!(context.subject, "test-subject");
620 assert_eq!(context.issuer, "test-issuer");
621 assert_eq!(context.metering_key, "meter-123");
622 }
623
624 fn account_claims() -> SessionClaims {
625 SessionClaims::builder("test-issuer", "anon:visitor", "test-audience")
626 .with_ttl(300)
627 .with_scope("read")
628 .with_metering_key("account:42")
629 .with_key_class(KeyClass::Publishable)
630 .with_plan("pro")
631 .with_limits(Limits {
632 max_connections: Some(10),
633 ..Limits::default()
634 })
635 .with_actor_key("user:7")
636 .with_account_key("account:42")
637 .with_consumer_key("consumer:abc")
638 .with_policy_version(4)
639 .with_account_limits(Limits {
640 max_connections: Some(500),
641 ..Limits::default()
642 })
643 .build()
644 }
645
646 fn decoded_payload(token: &str) -> serde_json::Value {
647 let payload = token.split('.').nth(1).unwrap();
648 serde_json::from_slice(
649 &base64::engine::general_purpose::URL_SAFE_NO_PAD
650 .decode(payload)
651 .unwrap(),
652 )
653 .unwrap()
654 }
655
656 #[test]
657 fn sealed_tokens_hide_account_claims_and_verify_with_the_key() {
658 let signing_key = crate::keys::SigningKey::generate();
659 let verifying_key = signing_key.verifying_key();
660 let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
661 let signer = TokenSigner::new(signing_key, "test-issuer");
662 let token = signer.sign_sealed(account_claims(), &seal).unwrap();
663
664 let payload = decoded_payload(&token);
665 for name in ACCOUNT_CLAIMS {
666 assert!(payload.get(*name).is_none(), "{name} visible in {payload}");
667 }
668 let mut clear = payload.as_object().unwrap().clone();
669 clear.remove(crate::sealed::SEALED_CLAIM);
670 let text = serde_json::to_string(&clear).unwrap();
671 for secret in ["account:42", "user:7", "pro", "500"] {
672 assert!(!text.contains(secret), "{secret} visible in {text}");
673 }
674 assert_eq!(payload["sub"], "anon:visitor");
675
676 let context = TokenVerifier::new(verifying_key.clone(), "test-issuer", "test-audience")
677 .with_claim_seal_keys(ClaimSealKeys::new(seal))
678 .verify(&token, None, None)
679 .unwrap();
680 assert_eq!(context.metering_key, "account:42");
681 assert_eq!(context.account_key(), "account:42");
682 assert_eq!(context.plan.as_deref(), Some("pro"));
683 assert_eq!(context.limits.max_connections, Some(10));
684 assert_eq!(context.account_limits.max_connections, Some(500));
685 assert_eq!(context.policy_version, Some(4));
686
687 let unkeyed = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
688 .verify(&token, None, None)
689 .unwrap_err();
690 assert_eq!(
691 unkeyed,
692 VerifyError::SealedClaims(crate::sealed::SealError::KeyRequired)
693 );
694 }
695
696 #[test]
697 fn sealed_claims_are_opened_only_after_the_signature_checks() {
698 let signing_key = crate::keys::SigningKey::generate();
699 let other = crate::keys::SigningKey::generate();
700 let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
701 let token = TokenSigner::new(other, "test-issuer")
702 .sign_sealed(account_claims(), &seal)
703 .unwrap();
704 let error = TokenVerifier::new(signing_key.verifying_key(), "test-issuer", "test-audience")
705 .with_claim_seal_keys(ClaimSealKeys::new(seal))
706 .verify(&token, None, None)
707 .unwrap_err();
708 assert_eq!(error, VerifyError::InvalidSignature);
709 }
710
711 #[test]
712 fn legacy_claims_without_typed_target_deserialize_and_verify() {
713 use std::time::{SystemTime, UNIX_EPOCH};
714
715 let now = SystemTime::now()
716 .duration_since(UNIX_EPOCH)
717 .unwrap()
718 .as_secs();
719 let claims: SessionClaims = serde_json::from_value(serde_json::json!({
720 "iss": "test-issuer",
721 "sub": "legacy-subject",
722 "aud": "deployment-1",
723 "iat": now,
724 "nbf": now,
725 "exp": now + 300,
726 "jti": "legacy-jti",
727 "scope": "read",
728 "metering_key": "api_key:1",
729 "deployment_id": "deployment-1",
730 "key_class": "publishable"
731 }))
732 .unwrap();
733 assert_eq!(claims.target_kind, None);
734 assert_eq!(claims.target_id, None);
735 assert_eq!(claims.program_id, None);
736 assert_eq!(claims.program_release_hash, None);
737
738 let signing_key = crate::keys::SigningKey::generate();
739 let verifying_key = signing_key.verifying_key();
740 let token = TokenSigner::new(signing_key, "test-issuer")
741 .sign(claims)
742 .unwrap();
743 let context = TokenVerifier::new(verifying_key, "test-issuer", "deployment-1")
744 .verify(&token, None, None)
745 .unwrap();
746
747 assert_eq!(context.subject, "legacy-subject");
748 assert_eq!(context.deployment_id.as_deref(), Some("deployment-1"));
749 assert_eq!(context.target_kind, None);
750 }
751
752 #[test]
753 fn verifier_rejects_partial_v2_policy_claims_and_accepts_full_sets() {
754 let signing_key = crate::keys::SigningKey::generate();
755 let verifying_key = signing_key.verifying_key();
756 let signer = TokenSigner::new(signing_key, "test-issuer");
757 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
758
759 let partial = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
762 .with_metering_key("account:42")
763 .with_account_key("account:42")
764 .build();
765 let token = signer.sign(partial).unwrap();
766 assert!(matches!(
767 verifier.verify(&token, None, None),
768 Err(VerifyError::InvalidPolicyClaims(_))
769 ));
770
771 let full = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
773 .with_metering_key("account:42")
774 .with_plan("pro")
775 .with_actor_key("user:1")
776 .with_account_key("account:42")
777 .with_consumer_key("consumer:abc123")
778 .with_policy_version(2)
779 .with_account_limits(Limits::default())
780 .build();
781 let token = signer.sign(full).unwrap();
782 let context = verifier.verify(&token, None, None).unwrap();
783 assert!(!context.is_legacy_policy());
784 assert_eq!(context.account_key(), "account:42");
785 assert_eq!(context.consumer_key(), "consumer:abc123");
786 assert_eq!(context.policy_version, Some(2));
787 }
788
789 #[test]
790 fn test_expired_token() {
791 let signing_key = crate::keys::SigningKey::generate();
792 let verifying_key = signing_key.verifying_key();
793
794 let signer = TokenSigner::new(signing_key, "test-issuer");
795 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
796
797 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
799 .with_ttl(0) .with_scope("read")
801 .with_metering_key("meter-123")
802 .with_key_class(KeyClass::Publishable)
803 .build();
804
805 let token = signer.sign(claims).unwrap();
806
807 let result = verifier.verify(&token, None, None);
809 assert!(matches!(result, Err(VerifyError::Expired)));
810 }
811
812 #[test]
813 fn test_future_issued_token_is_not_yet_valid() {
814 let signing_key = crate::keys::SigningKey::generate();
815 let verifying_key = signing_key.verifying_key();
816 let signer = TokenSigner::new(signing_key, "test-issuer");
817 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
818 let mut claims = create_test_claims();
819 claims.iat += 300;
820
821 let token = signer.sign(claims).unwrap();
822
823 assert!(matches!(
824 verifier.verify(&token, None, None),
825 Err(VerifyError::NotYetValid)
826 ));
827 }
828
829 #[test]
830 fn test_invalid_signature() {
831 let signing_key = crate::keys::SigningKey::generate();
832 let wrong_signing_key = crate::keys::SigningKey::generate();
833 let wrong_verifying_key = wrong_signing_key.verifying_key();
834
835 let signer = TokenSigner::new(signing_key, "test-issuer");
836 let verifier = TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
837
838 let claims = create_test_claims();
839 let token = signer.sign(claims).unwrap();
840
841 let result = verifier.verify(&token, None, None);
843 assert!(matches!(result, Err(VerifyError::InvalidSignature)));
844 }
845
846 #[test]
847 fn test_wrong_issuer() {
848 let signing_key = crate::keys::SigningKey::generate();
849 let verifying_key = signing_key.verifying_key();
850
851 let signer = TokenSigner::new(signing_key, "wrong-issuer");
852 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
853
854 let claims = SessionClaims::builder("wrong-issuer", "test-subject", "test-audience")
856 .with_ttl(300)
857 .with_scope("read")
858 .with_metering_key("meter-123")
859 .with_key_class(KeyClass::Publishable)
860 .build();
861 let token = signer.sign(claims).unwrap();
862
863 let result = verifier.verify(&token, None, None);
865 assert!(matches!(result, Err(VerifyError::InvalidIssuer)));
866 }
867
868 #[test]
869 #[allow(deprecated)]
870 fn the_single_audience_accessor_still_answers_for_scalar_verifiers() {
871 let verifying_key = crate::keys::SigningKey::generate().verifying_key();
873 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "deployment-31");
874 assert_eq!(verifier.audience(), "deployment-31");
875 assert_eq!(verifier.audiences().as_single(), Some("deployment-31"));
876 }
877
878 #[test]
881 fn a_multi_audience_verifier_accepts_each_and_reports_which_matched() {
882 let signing_key = crate::keys::SigningKey::generate();
883 let verifying_key = signing_key.verifying_key();
884 let signer = TokenSigner::new(signing_key, "test-issuer");
885
886 let verifier = TokenVerifier::with_audiences(
887 verifying_key,
888 "test-issuer",
889 ["deployment-31", "deployment-32"],
890 )
891 .expect("non-empty audience set");
892
893 for audience in ["deployment-31", "deployment-32"] {
894 let claims = SessionClaims::builder("test-issuer", "test-subject", audience)
895 .with_ttl(300)
896 .with_scope("read")
897 .with_metering_key("meter-123")
898 .with_key_class(KeyClass::Publishable)
899 .build();
900 let token = signer.sign(claims).unwrap();
901
902 let ctx = verifier
903 .verify(&token, None, None)
904 .unwrap_or_else(|error| panic!("{audience} should verify: {error:?}"));
905 assert_eq!(ctx.audience, audience);
907 }
908
909 let claims = SessionClaims::builder("test-issuer", "test-subject", "deployment-99")
912 .with_ttl(300)
913 .with_scope("read")
914 .with_metering_key("meter-123")
915 .with_key_class(KeyClass::Publishable)
916 .build();
917 let token = signer.sign(claims).unwrap();
918 assert!(matches!(
919 verifier.verify(&token, None, None),
920 Err(VerifyError::InvalidAudience)
921 ));
922 }
923
924 #[test]
925 fn test_wrong_audience() {
926 let signing_key = crate::keys::SigningKey::generate();
927 let verifying_key = signing_key.verifying_key();
928
929 let signer = TokenSigner::new(signing_key, "test-issuer");
930 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "expected-audience");
931
932 let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
933 .with_ttl(300)
934 .with_scope("read")
935 .with_metering_key("meter-123")
936 .with_key_class(KeyClass::Publishable)
937 .build();
938 let token = signer.sign(claims).unwrap();
939
940 let result = verifier.verify(&token, None, None);
941 assert!(matches!(result, Err(VerifyError::InvalidAudience)));
942 }
943
944 #[test]
945 fn test_origin_mismatch() {
946 let signing_key = crate::keys::SigningKey::generate();
947 let verifying_key = signing_key.verifying_key();
948
949 let signer = TokenSigner::new(signing_key, "test-issuer");
950 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
951 .with_origin_validation();
952
953 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
954 .with_ttl(300)
955 .with_scope("read")
956 .with_metering_key("meter-123")
957 .with_origin("https://allowed.example")
958 .with_key_class(KeyClass::Publishable)
959 .build();
960 let token = signer.sign(claims).unwrap();
961
962 let result = verifier.verify(&token, Some("https://other.example"), None);
963 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
964 }
965
966 #[test]
967 fn test_origin_validation_success() {
968 let signing_key = crate::keys::SigningKey::generate();
969 let verifying_key = signing_key.verifying_key();
970
971 let signer = TokenSigner::new(signing_key, "test-issuer");
972 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
973 .with_origin_validation();
974
975 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
976 .with_ttl(300)
977 .with_scope("read")
978 .with_metering_key("meter-123")
979 .with_origin("https://allowed.example")
980 .with_key_class(KeyClass::Publishable)
981 .build();
982 let token = signer.sign(claims).unwrap();
983
984 let context = verifier
985 .verify(&token, Some("https://allowed.example"), None)
986 .unwrap();
987 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
988 }
989
990 #[test]
991 fn test_origin_validation_requires_origin_claim() {
992 let signing_key = crate::keys::SigningKey::generate();
993 let verifying_key = signing_key.verifying_key();
994
995 let signer = TokenSigner::new(signing_key, "test-issuer");
996 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
997 .with_origin_validation();
998
999 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1000 .with_ttl(300)
1001 .with_scope("read")
1002 .with_metering_key("meter-123")
1003 .with_key_class(KeyClass::Publishable)
1004 .build();
1005 let token = signer.sign(claims).unwrap();
1006
1007 let result = verifier.verify(&token, None, None);
1008 assert!(matches!(
1009 result,
1010 Err(VerifyError::MissingClaim(ref claim)) if claim == "origin"
1011 ));
1012 }
1013
1014 #[test]
1015 fn test_client_ip_validation_requires_client_ip_claim() {
1016 let signing_key = crate::keys::SigningKey::generate();
1017 let verifying_key = signing_key.verifying_key();
1018
1019 let signer = TokenSigner::new(signing_key, "test-issuer");
1020 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1021 .with_client_ip_validation();
1022
1023 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1024 .with_ttl(300)
1025 .with_scope("read")
1026 .with_metering_key("meter-123")
1027 .with_key_class(KeyClass::Publishable)
1028 .build();
1029 let token = signer.sign(claims).unwrap();
1030
1031 let result = verifier.verify(&token, None, None);
1032 assert!(matches!(
1033 result,
1034 Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_origin_bound_token_allows_no_origin_when_not_required() {
1040 let signing_key = crate::keys::SigningKey::generate();
1046 let verifying_key = signing_key.verifying_key();
1047
1048 let signer = TokenSigner::new(signing_key, "test-issuer");
1049 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1051
1052 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1053 .with_ttl(300)
1054 .with_scope("read")
1055 .with_metering_key("meter-123")
1056 .with_origin("https://example.com") .with_key_class(KeyClass::Publishable)
1058 .build();
1059 let token = signer.sign(claims).unwrap();
1060
1061 let context = verifier.verify(&token, None, None).unwrap();
1063 assert_eq!(context.origin.as_deref(), Some("https://example.com"));
1064 }
1065
1066 #[test]
1067 fn test_origin_bound_token_validates_when_origin_provided_even_when_not_required() {
1068 let signing_key = crate::keys::SigningKey::generate();
1071 let verifying_key = signing_key.verifying_key();
1072
1073 let signer = TokenSigner::new(signing_key, "test-issuer");
1074 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1076
1077 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1078 .with_ttl(300)
1079 .with_scope("read")
1080 .with_metering_key("meter-123")
1081 .with_origin("https://allowed.example")
1082 .with_key_class(KeyClass::Publishable)
1083 .build();
1084 let token = signer.sign(claims).unwrap();
1085
1086 let context = verifier
1088 .verify(&token, Some("https://allowed.example"), None)
1089 .unwrap();
1090 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
1091
1092 let result = verifier.verify(&token, Some("https://evil.example"), None);
1094 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
1095 }
1096}