1use crate::claims::{AuthContext, SessionClaims};
2use crate::error::VerifyError;
3use crate::keys::{SigningKey, VerifyingKey};
4use base64::Engine;
5use serde::{Deserialize, Serialize};
6use serde_json;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10struct JwtHeader {
11 alg: String,
12 typ: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 kid: Option<String>,
15}
16
17impl Default for JwtHeader {
18 fn default() -> Self {
19 Self {
20 alg: "EdDSA".to_string(),
21 typ: "JWT".to_string(),
22 kid: None,
23 }
24 }
25}
26
27pub struct TokenSigner {
29 signing_key: SigningKey,
30 issuer: String,
31}
32
33impl TokenSigner {
34 pub fn new(signing_key: SigningKey, issuer: impl Into<String>) -> Self {
39 Self {
40 signing_key,
41 issuer: issuer.into(),
42 }
43 }
44
45 pub fn sign(&self, claims: SessionClaims) -> Result<String, TokenError> {
47 let header = JwtHeader {
49 kid: Some(self.signing_key.key_id()),
50 ..Default::default()
51 };
52
53 let header_json = serde_json::to_string(&header)?;
55 let header_b64 =
56 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header_json.as_bytes());
57
58 let claims_json = serde_json::to_string(&claims)?;
60 let claims_b64 =
61 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims_json.as_bytes());
62
63 let message = format!("{}.{}", header_b64, claims_b64);
65
66 let signature = self.signing_key.sign(message.as_bytes());
68 let signature_b64 =
69 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes());
70
71 Ok(format!("{}.{}.{}", header_b64, claims_b64, signature_b64))
73 }
74
75 pub fn issuer(&self) -> &str {
77 &self.issuer
78 }
79}
80
81#[derive(Debug)]
83pub enum TokenError {
84 Serialization(serde_json::Error),
85 Base64(base64::DecodeError),
86 InvalidFormat(String),
87}
88
89impl std::fmt::Display for TokenError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 TokenError::Serialization(e) => write!(f, "Serialization error: {}", e),
93 TokenError::Base64(e) => write!(f, "Base64 error: {}", e),
94 TokenError::InvalidFormat(s) => write!(f, "Invalid format: {}", s),
95 }
96 }
97}
98
99impl std::error::Error for TokenError {}
100
101impl From<serde_json::Error> for TokenError {
102 fn from(e: serde_json::Error) -> Self {
103 TokenError::Serialization(e)
104 }
105}
106
107impl From<base64::DecodeError> for TokenError {
108 fn from(e: base64::DecodeError) -> Self {
109 TokenError::Base64(e)
110 }
111}
112
113pub struct TokenVerifier {
115 verifying_key: VerifyingKey,
116 issuer: String,
117 audiences: crate::AudienceSet,
118 require_origin: bool,
119 require_client_ip: bool,
120}
121
122impl TokenVerifier {
123 pub fn new(
128 verifying_key: VerifyingKey,
129 issuer: impl Into<String>,
130 audience: impl Into<String>,
131 ) -> Self {
132 Self {
133 verifying_key,
134 issuer: issuer.into(),
135 audiences: crate::AudienceSet::single(audience),
136 require_origin: false,
137 require_client_ip: false,
138 }
139 }
140
141 pub fn with_audiences<I, S>(
149 verifying_key: VerifyingKey,
150 issuer: impl Into<String>,
151 audiences: I,
152 ) -> Result<Self, crate::AudienceSetError>
153 where
154 I: IntoIterator<Item = S>,
155 S: Into<String>,
156 {
157 Ok(Self {
158 verifying_key,
159 issuer: issuer.into(),
160 audiences: crate::AudienceSet::new(audiences)?,
161 require_origin: false,
162 require_client_ip: false,
163 })
164 }
165
166 pub fn with_origin_validation(mut self) -> Self {
168 self.require_origin = true;
169 self
170 }
171
172 pub fn with_client_ip_validation(mut self) -> Self {
174 self.require_client_ip = true;
175 self
176 }
177
178 pub fn verify(
185 &self,
186 token: &str,
187 expected_origin: Option<&str>,
188 expected_client_ip: Option<&str>,
189 ) -> Result<AuthContext, VerifyError> {
190 let parts: Vec<&str> = token.split('.').collect();
192 if parts.len() != 3 {
193 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
194 }
195
196 let header_b64 = parts[0];
197 let claims_b64 = parts[1];
198 let signature_b64 = parts[2];
199
200 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
202 .decode(header_b64)
203 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header base64: {}", e)))?;
204 let header: JwtHeader = serde_json::from_slice(&header_json)
205 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
206
207 if header.alg != "EdDSA" {
208 return Err(VerifyError::InvalidFormat(format!(
209 "Unsupported algorithm: {}",
210 header.alg
211 )));
212 }
213
214 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
216 .decode(claims_b64)
217 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims base64: {}", e)))?;
218 let claims: SessionClaims = serde_json::from_slice(&claims_json)
219 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
220
221 let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
223 .decode(signature_b64)
224 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid signature base64: {}", e)))?;
225 if signature_bytes.len() != 64 {
226 return Err(VerifyError::InvalidFormat(
227 "Invalid signature length".to_string(),
228 ));
229 }
230 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes.try_into().unwrap());
231
232 let message = format!("{}.{}", header_b64, claims_b64);
234 self.verifying_key
235 .verify(message.as_bytes(), &signature)
236 .map_err(|_| VerifyError::InvalidSignature)?;
237
238 if claims.iss != self.issuer {
240 return Err(VerifyError::InvalidIssuer);
241 }
242
243 if !self.audiences.accepts(&claims.aud) {
246 return Err(VerifyError::InvalidAudience);
247 }
248
249 use std::time::{SystemTime, UNIX_EPOCH};
251 let now = SystemTime::now()
252 .duration_since(UNIX_EPOCH)
253 .expect("time should not be before epoch")
254 .as_secs();
255
256 if claims.exp <= now {
257 return Err(VerifyError::Expired);
258 }
259
260 if claims.nbf > now || claims.iat > now {
261 return Err(VerifyError::NotYetValid);
262 }
263
264 let token_has_origin = claims.origin.is_some();
266 let origin_provided = expected_origin.is_some();
267
268 if token_has_origin && origin_provided {
269 let expected = expected_origin.unwrap();
271 let actual = claims.origin.as_ref().unwrap();
272
273 if actual != expected {
274 return Err(VerifyError::OriginMismatch {
275 expected: expected.to_string(),
276 actual: actual.clone(),
277 });
278 }
279 } else if token_has_origin && self.require_origin {
280 return Err(VerifyError::OriginRequired {
282 token_origin: claims.origin.as_ref().unwrap().clone(),
283 });
284 } else if !token_has_origin && self.require_origin {
285 return Err(VerifyError::MissingClaim("origin".to_string()));
287 }
288 if self.require_client_ip {
293 if let Some(expected) = expected_client_ip {
294 match &claims.client_ip {
295 Some(actual) if actual == expected => {}
296 Some(actual) => {
297 return Err(VerifyError::OriginMismatch {
298 expected: expected.to_string(),
299 actual: actual.clone(),
300 });
301 }
302 None => {
303 return Err(VerifyError::MissingClaim("client_ip".to_string()));
304 }
305 }
306 } else if claims.client_ip.is_none() {
307 return Err(VerifyError::MissingClaim("client_ip".to_string()));
308 }
309 }
310
311 claims
314 .validate_policy_claims()
315 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
316
317 Ok(AuthContext::from_claims(claims))
318 }
319
320 pub fn issuer(&self) -> &str {
322 &self.issuer
323 }
324
325 pub fn audiences(&self) -> &crate::AudienceSet {
327 &self.audiences
328 }
329
330 #[deprecated(
339 since = "0.5.0",
340 note = "use `audiences()`; a verifier may accept more than one audience"
341 )]
342 pub fn audience(&self) -> &str {
343 self.audiences.iter().next().unwrap_or_default()
344 }
345}
346
347#[derive(Debug, Clone, Deserialize)]
349pub struct Jwks {
350 pub keys: Vec<Jwk>,
351}
352
353#[derive(Debug, Clone, Deserialize)]
354pub struct Jwk {
355 pub kty: String,
356 #[serde(rename = "use")]
357 pub use_: Option<String>,
358 pub kid: String,
359 pub x: String, }
361
362#[derive(Clone)]
364pub struct JwksVerifier {
365 jwks: Jwks,
366 issuer: String,
367 audiences: crate::AudienceSet,
368 require_origin: bool,
369}
370
371impl JwksVerifier {
372 pub(crate) fn with_audience_set(
374 jwks: Jwks,
375 issuer: impl Into<String>,
376 audiences: crate::AudienceSet,
377 ) -> Self {
378 Self {
379 jwks,
380 issuer: issuer.into(),
381 audiences,
382 require_origin: false,
383 }
384 }
385
386 pub fn new(jwks: Jwks, issuer: impl Into<String>, audience: impl Into<String>) -> Self {
388 Self {
389 jwks,
390 issuer: issuer.into(),
391 audiences: crate::AudienceSet::single(audience),
392 require_origin: false,
393 }
394 }
395
396 pub fn with_origin_validation(mut self) -> Self {
398 self.require_origin = true;
399 self
400 }
401
402 pub fn verify(
404 &self,
405 token: &str,
406 expected_origin: Option<&str>,
407 expected_client_ip: Option<&str>,
408 ) -> Result<AuthContext, VerifyError> {
409 let parts: Vec<&str> = token.split('.').collect();
411 if parts.len() != 3 {
412 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
413 }
414
415 let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
416 .decode(parts[0])
417 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header: {}", e)))?;
418 let header: JwtHeader = serde_json::from_slice(&header_json)
419 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
420
421 let kid = header
422 .kid
423 .ok_or_else(|| VerifyError::MissingClaim("kid".to_string()))?;
424
425 let jwk = self
427 .jwks
428 .keys
429 .iter()
430 .find(|k| k.kid == kid)
431 .ok_or(VerifyError::KeyNotFound(kid))?;
432
433 let public_key_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
437 .decode(&jwk.x)
438 .map_err(|_| VerifyError::InvalidFormat("Invalid public key base64".to_string()))?;
439
440 let public_key: [u8; 32] = public_key_bytes
441 .try_into()
442 .map_err(|_| VerifyError::InvalidFormat("Invalid key length".to_string()))?;
443
444 let verifying_key = VerifyingKey::from_bytes(&public_key)
446 .map_err(|e| VerifyError::InvalidFormat(e.to_string()))?;
447
448 let verifier = TokenVerifier {
449 verifying_key,
450 issuer: self.issuer.clone(),
451 audiences: self.audiences.clone(),
452 require_origin: self.require_origin,
453 require_client_ip: false,
454 };
455
456 verifier.verify(token, expected_origin, expected_client_ip)
457 }
458
459 #[cfg(feature = "jwks")]
461 pub async fn fetch_jwks(url: &str) -> Result<Jwks, reqwest::Error> {
462 let response = reqwest::get(url).await?;
463 let jwks: Jwks = response.json().await?;
464 Ok(jwks)
465 }
466}
467
468#[cfg(test)]
469pub struct HmacVerifier {
471 _secret: Vec<u8>,
472 _issuer: String,
473 _audience: String,
474}
475
476#[cfg(test)]
477impl HmacVerifier {
478 pub fn new(
480 secret: impl Into<Vec<u8>>,
481 issuer: impl Into<String>,
482 audience: impl Into<String>,
483 ) -> Self {
484 Self {
485 _secret: secret.into(),
486 _issuer: issuer.into(),
487 _audience: audience.into(),
488 }
489 }
490
491 pub fn verify(
493 &self,
494 token: &str,
495 _expected_origin: Option<&str>,
496 ) -> Result<AuthContext, VerifyError> {
497 let parts: Vec<&str> = token.split('.').collect();
499 if parts.len() != 3 {
500 return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
501 }
502
503 let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
507 .decode(parts[1])
508 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims: {}", e)))?;
509 let claims: SessionClaims = serde_json::from_slice(&claims_json)
510 .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
511
512 claims
513 .validate_policy_claims()
514 .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
515
516 Ok(AuthContext::from_claims(claims))
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::claims::{KeyClass, Limits};
524
525 fn create_test_claims() -> SessionClaims {
526 SessionClaims::builder("test-issuer", "test-subject", "test-audience")
527 .with_ttl(300)
528 .with_scope("read")
529 .with_metering_key("meter-123")
530 .with_key_class(KeyClass::Publishable)
531 .with_limits(Limits {
532 max_connections: Some(10),
533 max_subscriptions: Some(100),
534 max_snapshot_rows: Some(1000),
535 max_messages_per_minute: Some(1000),
536 max_bytes_per_minute: Some(10_000_000),
537 max_http_requests_per_minute: Some(300),
538 max_http_batch_addresses: Some(100),
539 ..Limits::default()
540 })
541 .build()
542 }
543
544 #[test]
545 fn test_sign_and_verify() {
546 let signing_key = crate::keys::SigningKey::generate();
548 let verifying_key = signing_key.verifying_key();
549
550 let signer = TokenSigner::new(signing_key, "test-issuer");
552 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
553
554 let claims = create_test_claims();
556 let token = signer.sign(claims.clone()).unwrap();
557
558 let context = verifier.verify(&token, None, None).unwrap();
560
561 assert_eq!(context.subject, "test-subject");
562 assert_eq!(context.issuer, "test-issuer");
563 assert_eq!(context.metering_key, "meter-123");
564 }
565
566 #[test]
567 fn legacy_claims_without_typed_target_deserialize_and_verify() {
568 use std::time::{SystemTime, UNIX_EPOCH};
569
570 let now = SystemTime::now()
571 .duration_since(UNIX_EPOCH)
572 .unwrap()
573 .as_secs();
574 let claims: SessionClaims = serde_json::from_value(serde_json::json!({
575 "iss": "test-issuer",
576 "sub": "legacy-subject",
577 "aud": "deployment-1",
578 "iat": now,
579 "nbf": now,
580 "exp": now + 300,
581 "jti": "legacy-jti",
582 "scope": "read",
583 "metering_key": "api_key:1",
584 "deployment_id": "deployment-1",
585 "key_class": "publishable"
586 }))
587 .unwrap();
588 assert_eq!(claims.target_kind, None);
589 assert_eq!(claims.target_id, None);
590 assert_eq!(claims.program_id, None);
591 assert_eq!(claims.program_release_hash, None);
592
593 let signing_key = crate::keys::SigningKey::generate();
594 let verifying_key = signing_key.verifying_key();
595 let token = TokenSigner::new(signing_key, "test-issuer")
596 .sign(claims)
597 .unwrap();
598 let context = TokenVerifier::new(verifying_key, "test-issuer", "deployment-1")
599 .verify(&token, None, None)
600 .unwrap();
601
602 assert_eq!(context.subject, "legacy-subject");
603 assert_eq!(context.deployment_id.as_deref(), Some("deployment-1"));
604 assert_eq!(context.target_kind, None);
605 }
606
607 #[test]
608 fn verifier_rejects_partial_v2_policy_claims_and_accepts_full_sets() {
609 let signing_key = crate::keys::SigningKey::generate();
610 let verifying_key = signing_key.verifying_key();
611 let signer = TokenSigner::new(signing_key, "test-issuer");
612 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
613
614 let partial = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
617 .with_metering_key("account:42")
618 .with_account_key("account:42")
619 .build();
620 let token = signer.sign(partial).unwrap();
621 assert!(matches!(
622 verifier.verify(&token, None, None),
623 Err(VerifyError::InvalidPolicyClaims(_))
624 ));
625
626 let full = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
628 .with_metering_key("account:42")
629 .with_plan("pro")
630 .with_actor_key("user:1")
631 .with_account_key("account:42")
632 .with_consumer_key("consumer:abc123")
633 .with_policy_version(2)
634 .with_account_limits(Limits::default())
635 .build();
636 let token = signer.sign(full).unwrap();
637 let context = verifier.verify(&token, None, None).unwrap();
638 assert!(!context.is_legacy_policy());
639 assert_eq!(context.account_key(), "account:42");
640 assert_eq!(context.consumer_key(), "consumer:abc123");
641 assert_eq!(context.policy_version, Some(2));
642 }
643
644 #[test]
645 fn test_expired_token() {
646 let signing_key = crate::keys::SigningKey::generate();
647 let verifying_key = signing_key.verifying_key();
648
649 let signer = TokenSigner::new(signing_key, "test-issuer");
650 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
651
652 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
654 .with_ttl(0) .with_scope("read")
656 .with_metering_key("meter-123")
657 .with_key_class(KeyClass::Publishable)
658 .build();
659
660 let token = signer.sign(claims).unwrap();
661
662 let result = verifier.verify(&token, None, None);
664 assert!(matches!(result, Err(VerifyError::Expired)));
665 }
666
667 #[test]
668 fn test_future_issued_token_is_not_yet_valid() {
669 let signing_key = crate::keys::SigningKey::generate();
670 let verifying_key = signing_key.verifying_key();
671 let signer = TokenSigner::new(signing_key, "test-issuer");
672 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
673 let mut claims = create_test_claims();
674 claims.iat += 300;
675
676 let token = signer.sign(claims).unwrap();
677
678 assert!(matches!(
679 verifier.verify(&token, None, None),
680 Err(VerifyError::NotYetValid)
681 ));
682 }
683
684 #[test]
685 fn test_invalid_signature() {
686 let signing_key = crate::keys::SigningKey::generate();
687 let wrong_signing_key = crate::keys::SigningKey::generate();
688 let wrong_verifying_key = wrong_signing_key.verifying_key();
689
690 let signer = TokenSigner::new(signing_key, "test-issuer");
691 let verifier = TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
692
693 let claims = create_test_claims();
694 let token = signer.sign(claims).unwrap();
695
696 let result = verifier.verify(&token, None, None);
698 assert!(matches!(result, Err(VerifyError::InvalidSignature)));
699 }
700
701 #[test]
702 fn test_wrong_issuer() {
703 let signing_key = crate::keys::SigningKey::generate();
704 let verifying_key = signing_key.verifying_key();
705
706 let signer = TokenSigner::new(signing_key, "wrong-issuer");
707 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
708
709 let claims = SessionClaims::builder("wrong-issuer", "test-subject", "test-audience")
711 .with_ttl(300)
712 .with_scope("read")
713 .with_metering_key("meter-123")
714 .with_key_class(KeyClass::Publishable)
715 .build();
716 let token = signer.sign(claims).unwrap();
717
718 let result = verifier.verify(&token, None, None);
720 assert!(matches!(result, Err(VerifyError::InvalidIssuer)));
721 }
722
723 #[test]
724 #[allow(deprecated)]
725 fn the_single_audience_accessor_still_answers_for_scalar_verifiers() {
726 let verifying_key = crate::keys::SigningKey::generate().verifying_key();
728 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "deployment-31");
729 assert_eq!(verifier.audience(), "deployment-31");
730 assert_eq!(verifier.audiences().as_single(), Some("deployment-31"));
731 }
732
733 #[test]
736 fn a_multi_audience_verifier_accepts_each_and_reports_which_matched() {
737 let signing_key = crate::keys::SigningKey::generate();
738 let verifying_key = signing_key.verifying_key();
739 let signer = TokenSigner::new(signing_key, "test-issuer");
740
741 let verifier = TokenVerifier::with_audiences(
742 verifying_key,
743 "test-issuer",
744 ["deployment-31", "deployment-32"],
745 )
746 .expect("non-empty audience set");
747
748 for audience in ["deployment-31", "deployment-32"] {
749 let claims = SessionClaims::builder("test-issuer", "test-subject", audience)
750 .with_ttl(300)
751 .with_scope("read")
752 .with_metering_key("meter-123")
753 .with_key_class(KeyClass::Publishable)
754 .build();
755 let token = signer.sign(claims).unwrap();
756
757 let ctx = verifier
758 .verify(&token, None, None)
759 .unwrap_or_else(|error| panic!("{audience} should verify: {error:?}"));
760 assert_eq!(ctx.audience, audience);
762 }
763
764 let claims = SessionClaims::builder("test-issuer", "test-subject", "deployment-99")
767 .with_ttl(300)
768 .with_scope("read")
769 .with_metering_key("meter-123")
770 .with_key_class(KeyClass::Publishable)
771 .build();
772 let token = signer.sign(claims).unwrap();
773 assert!(matches!(
774 verifier.verify(&token, None, None),
775 Err(VerifyError::InvalidAudience)
776 ));
777 }
778
779 #[test]
780 fn test_wrong_audience() {
781 let signing_key = crate::keys::SigningKey::generate();
782 let verifying_key = signing_key.verifying_key();
783
784 let signer = TokenSigner::new(signing_key, "test-issuer");
785 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "expected-audience");
786
787 let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
788 .with_ttl(300)
789 .with_scope("read")
790 .with_metering_key("meter-123")
791 .with_key_class(KeyClass::Publishable)
792 .build();
793 let token = signer.sign(claims).unwrap();
794
795 let result = verifier.verify(&token, None, None);
796 assert!(matches!(result, Err(VerifyError::InvalidAudience)));
797 }
798
799 #[test]
800 fn test_origin_mismatch() {
801 let signing_key = crate::keys::SigningKey::generate();
802 let verifying_key = signing_key.verifying_key();
803
804 let signer = TokenSigner::new(signing_key, "test-issuer");
805 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
806 .with_origin_validation();
807
808 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
809 .with_ttl(300)
810 .with_scope("read")
811 .with_metering_key("meter-123")
812 .with_origin("https://allowed.example")
813 .with_key_class(KeyClass::Publishable)
814 .build();
815 let token = signer.sign(claims).unwrap();
816
817 let result = verifier.verify(&token, Some("https://other.example"), None);
818 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
819 }
820
821 #[test]
822 fn test_origin_validation_success() {
823 let signing_key = crate::keys::SigningKey::generate();
824 let verifying_key = signing_key.verifying_key();
825
826 let signer = TokenSigner::new(signing_key, "test-issuer");
827 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
828 .with_origin_validation();
829
830 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
831 .with_ttl(300)
832 .with_scope("read")
833 .with_metering_key("meter-123")
834 .with_origin("https://allowed.example")
835 .with_key_class(KeyClass::Publishable)
836 .build();
837 let token = signer.sign(claims).unwrap();
838
839 let context = verifier
840 .verify(&token, Some("https://allowed.example"), None)
841 .unwrap();
842 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
843 }
844
845 #[test]
846 fn test_origin_validation_requires_origin_claim() {
847 let signing_key = crate::keys::SigningKey::generate();
848 let verifying_key = signing_key.verifying_key();
849
850 let signer = TokenSigner::new(signing_key, "test-issuer");
851 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
852 .with_origin_validation();
853
854 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
855 .with_ttl(300)
856 .with_scope("read")
857 .with_metering_key("meter-123")
858 .with_key_class(KeyClass::Publishable)
859 .build();
860 let token = signer.sign(claims).unwrap();
861
862 let result = verifier.verify(&token, None, None);
863 assert!(matches!(
864 result,
865 Err(VerifyError::MissingClaim(ref claim)) if claim == "origin"
866 ));
867 }
868
869 #[test]
870 fn test_client_ip_validation_requires_client_ip_claim() {
871 let signing_key = crate::keys::SigningKey::generate();
872 let verifying_key = signing_key.verifying_key();
873
874 let signer = TokenSigner::new(signing_key, "test-issuer");
875 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
876 .with_client_ip_validation();
877
878 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
879 .with_ttl(300)
880 .with_scope("read")
881 .with_metering_key("meter-123")
882 .with_key_class(KeyClass::Publishable)
883 .build();
884 let token = signer.sign(claims).unwrap();
885
886 let result = verifier.verify(&token, None, None);
887 assert!(matches!(
888 result,
889 Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
890 ));
891 }
892
893 #[test]
894 fn test_origin_bound_token_allows_no_origin_when_not_required() {
895 let signing_key = crate::keys::SigningKey::generate();
901 let verifying_key = signing_key.verifying_key();
902
903 let signer = TokenSigner::new(signing_key, "test-issuer");
904 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
906
907 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
908 .with_ttl(300)
909 .with_scope("read")
910 .with_metering_key("meter-123")
911 .with_origin("https://example.com") .with_key_class(KeyClass::Publishable)
913 .build();
914 let token = signer.sign(claims).unwrap();
915
916 let context = verifier.verify(&token, None, None).unwrap();
918 assert_eq!(context.origin.as_deref(), Some("https://example.com"));
919 }
920
921 #[test]
922 fn test_origin_bound_token_validates_when_origin_provided_even_when_not_required() {
923 let signing_key = crate::keys::SigningKey::generate();
926 let verifying_key = signing_key.verifying_key();
927
928 let signer = TokenSigner::new(signing_key, "test-issuer");
929 let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
931
932 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
933 .with_ttl(300)
934 .with_scope("read")
935 .with_metering_key("meter-123")
936 .with_origin("https://allowed.example")
937 .with_key_class(KeyClass::Publishable)
938 .build();
939 let token = signer.sign(claims).unwrap();
940
941 let context = verifier
943 .verify(&token, Some("https://allowed.example"), None)
944 .unwrap();
945 assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
946
947 let result = verifier.verify(&token, Some("https://evil.example"), None);
949 assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
950 }
951}