Skip to main content

arete_auth/
token.rs

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/// JWT Header for EdDSA (Ed25519) tokens
9#[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
27/// Token signer for issuing session tokens using Ed25519 (EdDSA)
28pub struct TokenSigner {
29    signing_key: SigningKey,
30    issuer: String,
31}
32
33impl TokenSigner {
34    /// Create a new token signer with an Ed25519 signing key
35    ///
36    /// Uses EdDSA (Ed25519) for asymmetric signing. This is the recommended
37    /// algorithm for production use as it provides better security than HMAC.
38    pub fn new(signing_key: SigningKey, issuer: impl Into<String>) -> Self {
39        Self {
40            signing_key,
41            issuer: issuer.into(),
42        }
43    }
44
45    /// Sign a session token using Ed25519
46    pub fn sign(&self, claims: SessionClaims) -> Result<String, TokenError> {
47        // Create header with key ID
48        let header = JwtHeader {
49            kid: Some(self.signing_key.key_id()),
50            ..Default::default()
51        };
52
53        // Encode header
54        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        // Encode claims
59        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        // Create message to sign
64        let message = format!("{}.{}", header_b64, claims_b64);
65
66        // Sign with Ed25519
67        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        // Combine into JWT
72        Ok(format!("{}.{}.{}", header_b64, claims_b64, signature_b64))
73    }
74
75    /// Get the issuer
76    pub fn issuer(&self) -> &str {
77        &self.issuer
78    }
79}
80
81/// Token error type
82#[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
113/// Token verifier for validating session tokens using Ed25519 (EdDSA)
114pub 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    /// Create a new token verifier with an Ed25519 verifying key
124    ///
125    /// Uses EdDSA (Ed25519) for asymmetric signature verification.
126    /// This is the recommended algorithm for production use.
127    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    /// Create a verifier that accepts any audience in `audiences`.
142    ///
143    /// The matched audience reaches callers as
144    /// [`crate::claims::AuthContext::audience`], so a verifier serving several
145    /// audiences can tell which one a token was minted for. Returns an error
146    /// rather than defaulting to something permissive when the set is empty or
147    /// blank.
148    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    /// Require origin validation
167    pub fn with_origin_validation(mut self) -> Self {
168        self.require_origin = true;
169        self
170    }
171
172    /// Require client IP validation
173    pub fn with_client_ip_validation(mut self) -> Self {
174        self.require_client_ip = true;
175        self
176    }
177
178    /// Verify a token and return the auth context
179    ///
180    /// # Arguments
181    /// * `token` - The JWT token to verify
182    /// * `expected_origin` - Optional expected origin for origin validation
183    /// * `expected_client_ip` - Optional expected client IP for IP binding validation
184    pub fn verify(
185        &self,
186        token: &str,
187        expected_origin: Option<&str>,
188        expected_client_ip: Option<&str>,
189    ) -> Result<AuthContext, VerifyError> {
190        // Split token into parts
191        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        // Decode and verify header
201        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        // Decode claims
215        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        // Decode signature
222        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        // Verify signature
233        let message = format!("{}.{}", header_b64, claims_b64);
234        self.verifying_key
235            .verify(message.as_bytes(), &signature)
236            .map_err(|_| VerifyError::InvalidSignature)?;
237
238        // Check issuer
239        if claims.iss != self.issuer {
240            return Err(VerifyError::InvalidIssuer);
241        }
242
243        // Check audience. Exact membership: a loose comparison would accept a
244        // token minted for a different audience.
245        if !self.audiences.accepts(&claims.aud) {
246            return Err(VerifyError::InvalidAudience);
247        }
248
249        // Check expiration
250        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        // Validate origin if required or if token has origin binding
265        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            // Token is origin-bound and origin was provided - validate they match
270            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            // Token has origin but none was provided, and origin is required
281            return Err(VerifyError::OriginRequired {
282                token_origin: claims.origin.as_ref().unwrap().clone(),
283            });
284        } else if !token_has_origin && self.require_origin {
285            // Verifier requires origin but token doesn't have one bound
286            return Err(VerifyError::MissingClaim("origin".to_string()));
287        }
288        // If token has origin but none provided, and origin is NOT required,
289        // we allow the connection (defense-in-depth is optional)
290
291        // Validate client IP if required
292        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        // Validate the v2 policy identity set; old tokens with none of the
312        // new fields pass unchanged.
313        claims
314            .validate_policy_claims()
315            .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
316
317        Ok(AuthContext::from_claims(claims))
318    }
319
320    /// Get the expected issuer
321    pub fn issuer(&self) -> &str {
322        &self.issuer
323    }
324
325    /// The accepted audiences.
326    pub fn audiences(&self) -> &crate::AudienceSet {
327        &self.audiences
328    }
329
330    /// The sole accepted audience.
331    ///
332    /// Retained for source compatibility: every verifier built through
333    /// [`Self::new`] has exactly one audience, so this keeps returning what it
334    /// always did. A verifier built with [`Self::with_audiences`] has no single
335    /// audience — this then returns the first in sorted order, which is why it
336    /// is deprecated in favour of [`Self::audiences`]. That path is
337    /// unreachable for code written before multi-audience verifiers existed.
338    #[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/// JWKS structure for key rotation
348#[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, // Base64-encoded public key
360}
361
362/// Token verifier with JWKS support for key rotation
363#[derive(Clone)]
364pub struct JwksVerifier {
365    jwks: Jwks,
366    issuer: String,
367    audiences: crate::AudienceSet,
368    require_origin: bool,
369}
370
371impl JwksVerifier {
372    /// Create a JWKS verifier over an existing accepted-audience set.
373    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    /// Create a new JWKS verifier
387    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    /// Require origin validation
397    pub fn with_origin_validation(mut self) -> Self {
398        self.require_origin = true;
399        self
400    }
401
402    /// Verify a token using the appropriate key from JWKS
403    pub fn verify(
404        &self,
405        token: &str,
406        expected_origin: Option<&str>,
407        expected_client_ip: Option<&str>,
408    ) -> Result<AuthContext, VerifyError> {
409        // Decode header to get kid
410        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        // Find the key
426        let jwk = self
427            .jwks
428            .keys
429            .iter()
430            .find(|k| k.kid == kid)
431            .ok_or(VerifyError::KeyNotFound(kid))?;
432
433        // Decode the public key from hex (first 16 chars of hex = 8 bytes of key id)
434        // Actually, we need to decode the full public key from the JWKS
435        // The JWKS should contain the full base64-encoded public key
436        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        // Create verifier for this key
445        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    /// Fetch JWKS from a URL
460    #[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)]
469/// HMAC-based verifier for tests only
470pub struct HmacVerifier {
471    _secret: Vec<u8>,
472    _issuer: String,
473    _audience: String,
474}
475
476#[cfg(test)]
477impl HmacVerifier {
478    /// Create a new HMAC verifier (dev only)
479    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    /// Verify a token using HMAC
492    pub fn verify(
493        &self,
494        token: &str,
495        _expected_origin: Option<&str>,
496    ) -> Result<AuthContext, VerifyError> {
497        // Split token
498        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        // For HMAC, we'd need to verify the HMAC signature
504        // This is a simplified implementation - in practice you'd use hmac-sha256
505        // For now, just decode the claims without verification (dev only!)
506        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        // Generate keys
547        let signing_key = crate::keys::SigningKey::generate();
548        let verifying_key = signing_key.verifying_key();
549
550        // Create signer and verifier
551        let signer = TokenSigner::new(signing_key, "test-issuer");
552        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
553
554        // Sign token
555        let claims = create_test_claims();
556        let token = signer.sign(claims.clone()).unwrap();
557
558        // Verify token
559        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        // A signed token with only a subset of the v2 identity fields fails
615        // verification at the authorization boundary.
616        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        // The complete authenticated tuple verifies and resolves.
627        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        // Create expired claims
653        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
654            .with_ttl(0) // Already expired
655            .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        // Should fail with expired error
663        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        // Should fail with invalid signature
697        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        // Create claims with the wrong issuer
710        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        // Should fail with invalid issuer
719        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        // Source compatibility: this is what every pre-existing caller uses.
727        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    /// One verifier serving several audiences accepts each of them, and the
734    /// *matched* audience tells the caller which one a token was minted for.
735    #[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            // The caller learns which audience matched.
761            assert_eq!(ctx.audience, audience);
762        }
763
764        // An audience outside the set is still rejected, which is what stops a
765        // validly signed token being accepted where it does not belong.
766        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        // This tests the non-browser client scenario (Rust, Python, etc.)
896        // where the client doesn't send an Origin header, but the JWT has
897        // an origin claim from when the token was minted via browser/API.
898        // When require_origin is false, the connection should still be allowed
899        // for defense-in-depth flexibility.
900        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        // Verifier WITHOUT origin validation (the default for public stacks)
905        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") // Token has origin claim
912            .with_key_class(KeyClass::Publishable)
913            .build();
914        let token = signer.sign(claims).unwrap();
915
916        // No origin provided, but require_origin is false - should succeed
917        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        // When origin IS provided, it should still be validated against the token
924        // even when require_origin is false (defense-in-depth)
925        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        // Verifier WITHOUT origin validation (the default)
930        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        // Origin provided and matches - should succeed
942        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        // Origin provided but doesn't match - should fail
948        let result = verifier.verify(&token, Some("https://evil.example"), None);
949        assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
950    }
951}