Skip to main content

arete_auth/
token.rs

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/// JWT Header for EdDSA (Ed25519) tokens
10#[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
28/// Token signer for issuing session tokens using Ed25519 (EdDSA)
29pub struct TokenSigner {
30    signing_key: SigningKey,
31    issuer: String,
32}
33
34impl TokenSigner {
35    /// Create a new token signer with an Ed25519 signing key
36    ///
37    /// Uses EdDSA (Ed25519) for asymmetric signing. This is the recommended
38    /// algorithm for production use as it provides better security than HMAC.
39    pub fn new(signing_key: SigningKey, issuer: impl Into<String>) -> Self {
40        Self {
41            signing_key,
42            issuer: issuer.into(),
43        }
44    }
45
46    /// Sign a session token using Ed25519
47    pub fn sign(&self, claims: SessionClaims) -> Result<String, TokenError> {
48        self.sign_value(&serde_json::to_value(&claims)?)
49    }
50
51    /// Sign a session token whose account-only claims ([`ACCOUNT_CLAIMS`])
52    /// are sealed with `key`, for tokens handed to a party that should not
53    /// read them, such as a browser visitor.
54    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        // Create header with key ID
70        let header = JwtHeader {
71            kid: Some(self.signing_key.key_id()),
72            ..Default::default()
73        };
74
75        // Encode header
76        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        // Encode claims
81        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        // Create message to sign
86        let message = format!("{}.{}", header_b64, claims_b64);
87
88        // Sign with Ed25519
89        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        // Combine into JWT
94        Ok(format!("{}.{}.{}", header_b64, claims_b64, signature_b64))
95    }
96
97    /// Get the issuer
98    pub fn issuer(&self) -> &str {
99        &self.issuer
100    }
101}
102
103/// Token error type
104#[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
135/// Token verifier for validating session tokens using Ed25519 (EdDSA)
136pub 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    /// Create a new token verifier with an Ed25519 verifying key
147    ///
148    /// Uses EdDSA (Ed25519) for asymmetric signature verification.
149    /// This is the recommended algorithm for production use.
150    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    /// Create a verifier that accepts any audience in `audiences`.
166    ///
167    /// The matched audience reaches callers as
168    /// [`crate::claims::AuthContext::audience`], so a verifier serving several
169    /// audiences can tell which one a token was minted for. Returns an error
170    /// rather than defaulting to something permissive when the set is empty or
171    /// blank.
172    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    /// Require origin validation
192    pub fn with_origin_validation(mut self) -> Self {
193        self.require_origin = true;
194        self
195    }
196
197    /// Require client IP validation.
198    ///
199    /// Tokens must carry a `client_ip` claim equal to the `expected_client_ip`
200    /// passed to [`Self::verify`]. A call with no expected client IP is
201    /// refused rather than skipping the check.
202    pub fn with_client_ip_validation(mut self) -> Self {
203        self.require_client_ip = true;
204        self
205    }
206
207    /// Open sealed claims with these keys. Without them, a token that carries
208    /// sealed claims is refused.
209    pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
210        self.claim_seal_keys = Some(keys);
211        self
212    }
213
214    /// Verify a token and return the auth context
215    ///
216    /// # Arguments
217    /// * `token` - The JWT token to verify
218    /// * `expected_origin` - Optional expected origin for origin validation
219    /// * `expected_client_ip` - Optional expected client IP for IP binding validation
220    pub fn verify(
221        &self,
222        token: &str,
223        expected_origin: Option<&str>,
224        expected_client_ip: Option<&str>,
225    ) -> Result<AuthContext, VerifyError> {
226        // Split token into parts
227        let parts: Vec<&str> = token.split('.').collect();
228        if parts.len() != 3 {
229            return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
230        }
231
232        let header_b64 = parts[0];
233        let claims_b64 = parts[1];
234        let signature_b64 = parts[2];
235
236        // Decode and verify header
237        let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
238            .decode(header_b64)
239            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header base64: {}", e)))?;
240        let header: JwtHeader = serde_json::from_slice(&header_json)
241            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
242
243        if header.alg != "EdDSA" {
244            return Err(VerifyError::InvalidFormat(format!(
245                "Unsupported algorithm: {}",
246                header.alg
247            )));
248        }
249
250        // Decode claims
251        let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
252            .decode(claims_b64)
253            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims base64: {}", e)))?;
254
255        // Decode signature
256        let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
257            .decode(signature_b64)
258            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid signature base64: {}", e)))?;
259        if signature_bytes.len() != 64 {
260            return Err(VerifyError::InvalidFormat(
261                "Invalid signature length".to_string(),
262            ));
263        }
264        let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes.try_into().unwrap());
265
266        // Verify signature
267        let message = format!("{}.{}", header_b64, claims_b64);
268        self.verifying_key
269            .verify(message.as_bytes(), &signature)
270            .map_err(|_| VerifyError::InvalidSignature)?;
271
272        // Only a signed token's sealed claims are opened.
273        let claims = decode_claims(&claims_json, self.claim_seal_keys.as_ref())?;
274
275        // Check issuer
276        if claims.iss != self.issuer {
277            return Err(VerifyError::InvalidIssuer);
278        }
279
280        // Check audience. Exact membership: a loose comparison would accept a
281        // token minted for a different audience.
282        if !self.audiences.accepts(&claims.aud) {
283            return Err(VerifyError::InvalidAudience);
284        }
285
286        // Check expiration
287        use std::time::{SystemTime, UNIX_EPOCH};
288        let now = SystemTime::now()
289            .duration_since(UNIX_EPOCH)
290            .expect("time should not be before epoch")
291            .as_secs();
292
293        if claims.exp <= now {
294            return Err(VerifyError::Expired);
295        }
296
297        if claims.nbf > now || claims.iat > now {
298            return Err(VerifyError::NotYetValid);
299        }
300
301        // Validate origin if required or if token has origin binding
302        let token_has_origin = claims.origin.is_some();
303        let origin_provided = expected_origin.is_some();
304
305        if token_has_origin && origin_provided {
306            // Token is origin-bound and origin was provided - validate they match
307            let expected = expected_origin.unwrap();
308            let actual = claims.origin.as_ref().unwrap();
309
310            if actual != expected {
311                return Err(VerifyError::OriginMismatch {
312                    expected: expected.to_string(),
313                    actual: actual.clone(),
314                });
315            }
316        } else if token_has_origin && self.require_origin {
317            // Token has origin but none was provided, and origin is required
318            return Err(VerifyError::OriginRequired {
319                token_origin: claims.origin.as_ref().unwrap().clone(),
320            });
321        } else if !token_has_origin && self.require_origin {
322            // Verifier requires origin but token doesn't have one bound
323            return Err(VerifyError::MissingClaim("origin".to_string()));
324        }
325        // If token has origin but none provided, and origin is NOT required,
326        // we allow the connection (defense-in-depth is optional)
327
328        // Validate client IP if required. Fails closed: an IP-bound token is
329        // refused when the caller cannot say which IP the request came from.
330        if self.require_client_ip {
331            let actual = claims
332                .client_ip
333                .as_ref()
334                .ok_or_else(|| VerifyError::MissingClaim("client_ip".to_string()))?;
335            let expected = expected_client_ip
336                .ok_or_else(|| VerifyError::MissingClaim("client_ip".to_string()))?;
337            if actual != expected {
338                return Err(VerifyError::OriginMismatch {
339                    expected: expected.to_string(),
340                    actual: actual.clone(),
341                });
342            }
343        }
344
345        // Validate the v2 policy identity set; old tokens with none of the
346        // new fields pass unchanged.
347        claims
348            .validate_policy_claims()
349            .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
350
351        Ok(AuthContext::from_claims(claims))
352    }
353
354    /// Get the expected issuer
355    pub fn issuer(&self) -> &str {
356        &self.issuer
357    }
358
359    /// The accepted audiences.
360    pub fn audiences(&self) -> &crate::AudienceSet {
361        &self.audiences
362    }
363
364    /// The sole accepted audience.
365    ///
366    /// Retained for source compatibility: every verifier built through
367    /// [`Self::new`] has exactly one audience, so this keeps returning what it
368    /// always did. A verifier built with [`Self::with_audiences`] has no single
369    /// audience — this then returns the first in sorted order, which is why it
370    /// is deprecated in favour of [`Self::audiences`]. That path is
371    /// unreachable for code written before multi-audience verifiers existed.
372    #[deprecated(
373        since = "0.5.0",
374        note = "use `audiences()`; a verifier may accept more than one audience"
375    )]
376    pub fn audience(&self) -> &str {
377        self.audiences.iter().next().unwrap_or_default()
378    }
379}
380
381/// Parse a signed token's claims, opening sealed claims with `keys`.
382pub(crate) fn decode_claims(
383    claims_json: &[u8],
384    keys: Option<&ClaimSealKeys>,
385) -> Result<SessionClaims, VerifyError> {
386    let mut claims: serde_json::Value = serde_json::from_slice(claims_json)
387        .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
388    let object = claims
389        .as_object_mut()
390        .ok_or_else(|| VerifyError::InvalidFormat("Invalid claims JSON: not an object".into()))?;
391    unseal_claims(object, keys)?;
392    serde_json::from_value(claims)
393        .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))
394}
395
396/// JWKS structure for key rotation
397#[derive(Debug, Clone, Deserialize)]
398pub struct Jwks {
399    pub keys: Vec<Jwk>,
400}
401
402#[derive(Debug, Clone, Deserialize)]
403pub struct Jwk {
404    pub kty: String,
405    #[serde(rename = "use")]
406    pub use_: Option<String>,
407    pub kid: String,
408    pub x: String, // Base64-encoded public key
409}
410
411/// Token verifier with JWKS support for key rotation
412#[derive(Clone)]
413pub struct JwksVerifier {
414    jwks: Jwks,
415    issuer: String,
416    audiences: crate::AudienceSet,
417    require_origin: bool,
418    claim_seal_keys: Option<ClaimSealKeys>,
419}
420
421impl JwksVerifier {
422    /// Create a JWKS verifier over an existing accepted-audience set.
423    pub(crate) fn with_audience_set(
424        jwks: Jwks,
425        issuer: impl Into<String>,
426        audiences: crate::AudienceSet,
427    ) -> Self {
428        Self {
429            jwks,
430            issuer: issuer.into(),
431            audiences,
432            require_origin: false,
433            claim_seal_keys: None,
434        }
435    }
436
437    /// Create a new JWKS verifier
438    pub fn new(jwks: Jwks, issuer: impl Into<String>, audience: impl Into<String>) -> Self {
439        Self {
440            jwks,
441            issuer: issuer.into(),
442            audiences: crate::AudienceSet::single(audience),
443            require_origin: false,
444            claim_seal_keys: None,
445        }
446    }
447
448    /// Require origin validation
449    pub fn with_origin_validation(mut self) -> Self {
450        self.require_origin = true;
451        self
452    }
453
454    /// Open sealed claims with these keys.
455    pub fn with_claim_seal_keys(mut self, keys: ClaimSealKeys) -> Self {
456        self.claim_seal_keys = Some(keys);
457        self
458    }
459
460    /// Verify a token using the appropriate key from JWKS
461    pub fn verify(
462        &self,
463        token: &str,
464        expected_origin: Option<&str>,
465        expected_client_ip: Option<&str>,
466    ) -> Result<AuthContext, VerifyError> {
467        // Decode header to get kid
468        let parts: Vec<&str> = token.split('.').collect();
469        if parts.len() != 3 {
470            return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
471        }
472
473        let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
474            .decode(parts[0])
475            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header: {}", e)))?;
476        let header: JwtHeader = serde_json::from_slice(&header_json)
477            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid header JSON: {}", e)))?;
478
479        let kid = header
480            .kid
481            .ok_or_else(|| VerifyError::MissingClaim("kid".to_string()))?;
482
483        // Find the key
484        let jwk = self
485            .jwks
486            .keys
487            .iter()
488            .find(|k| k.kid == kid)
489            .ok_or(VerifyError::KeyNotFound(kid))?;
490
491        // Decode the public key from hex (first 16 chars of hex = 8 bytes of key id)
492        // Actually, we need to decode the full public key from the JWKS
493        // The JWKS should contain the full base64-encoded public key
494        let public_key_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
495            .decode(&jwk.x)
496            .map_err(|_| VerifyError::InvalidFormat("Invalid public key base64".to_string()))?;
497
498        let public_key: [u8; 32] = public_key_bytes
499            .try_into()
500            .map_err(|_| VerifyError::InvalidFormat("Invalid key length".to_string()))?;
501
502        // Create verifier for this key
503        let verifying_key = VerifyingKey::from_bytes(&public_key)
504            .map_err(|e| VerifyError::InvalidFormat(e.to_string()))?;
505
506        let verifier = TokenVerifier {
507            verifying_key,
508            issuer: self.issuer.clone(),
509            audiences: self.audiences.clone(),
510            require_origin: self.require_origin,
511            require_client_ip: false,
512            claim_seal_keys: self.claim_seal_keys.clone(),
513        };
514
515        verifier.verify(token, expected_origin, expected_client_ip)
516    }
517
518    /// Fetch JWKS from a URL
519    #[cfg(feature = "jwks")]
520    pub async fn fetch_jwks(url: &str) -> Result<Jwks, reqwest::Error> {
521        let response = reqwest::get(url).await?;
522        let jwks: Jwks = response.json().await?;
523        Ok(jwks)
524    }
525}
526
527#[cfg(test)]
528/// HMAC-based verifier for tests only
529pub struct HmacVerifier {
530    _secret: Vec<u8>,
531    _issuer: String,
532    _audience: String,
533}
534
535#[cfg(test)]
536impl HmacVerifier {
537    /// Create a new HMAC verifier (dev only)
538    pub fn new(
539        secret: impl Into<Vec<u8>>,
540        issuer: impl Into<String>,
541        audience: impl Into<String>,
542    ) -> Self {
543        Self {
544            _secret: secret.into(),
545            _issuer: issuer.into(),
546            _audience: audience.into(),
547        }
548    }
549
550    /// Verify a token using HMAC
551    pub fn verify(
552        &self,
553        token: &str,
554        _expected_origin: Option<&str>,
555    ) -> Result<AuthContext, VerifyError> {
556        // Split token
557        let parts: Vec<&str> = token.split('.').collect();
558        if parts.len() != 3 {
559            return Err(VerifyError::InvalidFormat("Invalid JWT format".to_string()));
560        }
561
562        // For HMAC, we'd need to verify the HMAC signature
563        // This is a simplified implementation - in practice you'd use hmac-sha256
564        // For now, just decode the claims without verification (dev only!)
565        let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
566            .decode(parts[1])
567            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims: {}", e)))?;
568        let claims: SessionClaims = serde_json::from_slice(&claims_json)
569            .map_err(|e| VerifyError::InvalidFormat(format!("Invalid claims JSON: {}", e)))?;
570
571        claims
572            .validate_policy_claims()
573            .map_err(|error| VerifyError::InvalidPolicyClaims(error.to_string()))?;
574
575        Ok(AuthContext::from_claims(claims))
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::claims::{KeyClass, Limits};
583
584    fn create_test_claims() -> SessionClaims {
585        SessionClaims::builder("test-issuer", "test-subject", "test-audience")
586            .with_ttl(300)
587            .with_scope("read")
588            .with_metering_key("meter-123")
589            .with_key_class(KeyClass::Publishable)
590            .with_limits(Limits {
591                max_connections: Some(10),
592                max_subscriptions: Some(100),
593                max_snapshot_rows: Some(1000),
594                max_messages_per_minute: Some(1000),
595                max_bytes_per_minute: Some(10_000_000),
596                max_http_requests_per_minute: Some(300),
597                max_http_batch_addresses: Some(100),
598                ..Limits::default()
599            })
600            .build()
601    }
602
603    #[test]
604    fn test_sign_and_verify() {
605        // Generate keys
606        let signing_key = crate::keys::SigningKey::generate();
607        let verifying_key = signing_key.verifying_key();
608
609        // Create signer and verifier
610        let signer = TokenSigner::new(signing_key, "test-issuer");
611        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
612
613        // Sign token
614        let claims = create_test_claims();
615        let token = signer.sign(claims.clone()).unwrap();
616
617        // Verify token
618        let context = verifier.verify(&token, None, None).unwrap();
619
620        assert_eq!(context.subject, "test-subject");
621        assert_eq!(context.issuer, "test-issuer");
622        assert_eq!(context.metering_key, "meter-123");
623    }
624
625    fn account_claims() -> SessionClaims {
626        SessionClaims::builder("test-issuer", "anon:visitor", "test-audience")
627            .with_ttl(300)
628            .with_scope("read")
629            .with_metering_key("account:42")
630            .with_key_class(KeyClass::Publishable)
631            .with_plan("pro")
632            .with_limits(Limits {
633                max_connections: Some(10),
634                ..Limits::default()
635            })
636            .with_actor_key("user:7")
637            .with_account_key("account:42")
638            .with_consumer_key("consumer:abc")
639            .with_policy_version(4)
640            .with_account_limits(Limits {
641                max_connections: Some(500),
642                ..Limits::default()
643            })
644            .build()
645    }
646
647    fn decoded_payload(token: &str) -> serde_json::Value {
648        let payload = token.split('.').nth(1).unwrap();
649        serde_json::from_slice(
650            &base64::engine::general_purpose::URL_SAFE_NO_PAD
651                .decode(payload)
652                .unwrap(),
653        )
654        .unwrap()
655    }
656
657    #[test]
658    fn sealed_tokens_hide_account_claims_and_verify_with_the_key() {
659        let signing_key = crate::keys::SigningKey::generate();
660        let verifying_key = signing_key.verifying_key();
661        let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
662        let signer = TokenSigner::new(signing_key, "test-issuer");
663        let token = signer.sign_sealed(account_claims(), &seal).unwrap();
664
665        let payload = decoded_payload(&token);
666        for name in ACCOUNT_CLAIMS {
667            assert!(payload.get(*name).is_none(), "{name} visible in {payload}");
668        }
669        let mut clear = payload.as_object().unwrap().clone();
670        clear.remove(crate::sealed::SEALED_CLAIM);
671        let text = serde_json::to_string(&clear).unwrap();
672        for secret in ["account:42", "user:7", "pro", "500"] {
673            assert!(!text.contains(secret), "{secret} visible in {text}");
674        }
675        assert_eq!(payload["sub"], "anon:visitor");
676
677        let context = TokenVerifier::new(verifying_key.clone(), "test-issuer", "test-audience")
678            .with_claim_seal_keys(ClaimSealKeys::new(seal))
679            .verify(&token, None, None)
680            .unwrap();
681        assert_eq!(context.metering_key, "account:42");
682        assert_eq!(context.account_key(), "account:42");
683        assert_eq!(context.plan.as_deref(), Some("pro"));
684        assert_eq!(context.limits.max_connections, Some(10));
685        assert_eq!(context.account_limits.max_connections, Some(500));
686        assert_eq!(context.policy_version, Some(4));
687
688        let unkeyed = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
689            .verify(&token, None, None)
690            .unwrap_err();
691        assert_eq!(
692            unkeyed,
693            VerifyError::SealedClaims(crate::sealed::SealError::KeyRequired)
694        );
695    }
696
697    #[test]
698    fn sealed_claims_are_opened_only_after_the_signature_checks() {
699        let signing_key = crate::keys::SigningKey::generate();
700        let other = crate::keys::SigningKey::generate();
701        let seal = ClaimSealKey::new("k1", [9; 32]).unwrap();
702        let token = TokenSigner::new(other, "test-issuer")
703            .sign_sealed(account_claims(), &seal)
704            .unwrap();
705        let error = TokenVerifier::new(signing_key.verifying_key(), "test-issuer", "test-audience")
706            .with_claim_seal_keys(ClaimSealKeys::new(seal))
707            .verify(&token, None, None)
708            .unwrap_err();
709        assert_eq!(error, VerifyError::InvalidSignature);
710    }
711
712    #[test]
713    fn legacy_claims_without_typed_target_deserialize_and_verify() {
714        use std::time::{SystemTime, UNIX_EPOCH};
715
716        let now = SystemTime::now()
717            .duration_since(UNIX_EPOCH)
718            .unwrap()
719            .as_secs();
720        let claims: SessionClaims = serde_json::from_value(serde_json::json!({
721            "iss": "test-issuer",
722            "sub": "legacy-subject",
723            "aud": "deployment-1",
724            "iat": now,
725            "nbf": now,
726            "exp": now + 300,
727            "jti": "legacy-jti",
728            "scope": "read",
729            "metering_key": "api_key:1",
730            "deployment_id": "deployment-1",
731            "key_class": "publishable"
732        }))
733        .unwrap();
734        assert_eq!(claims.target_kind, None);
735        assert_eq!(claims.target_id, None);
736        assert_eq!(claims.program_id, None);
737        assert_eq!(claims.program_release_hash, None);
738
739        let signing_key = crate::keys::SigningKey::generate();
740        let verifying_key = signing_key.verifying_key();
741        let token = TokenSigner::new(signing_key, "test-issuer")
742            .sign(claims)
743            .unwrap();
744        let context = TokenVerifier::new(verifying_key, "test-issuer", "deployment-1")
745            .verify(&token, None, None)
746            .unwrap();
747
748        assert_eq!(context.subject, "legacy-subject");
749        assert_eq!(context.deployment_id.as_deref(), Some("deployment-1"));
750        assert_eq!(context.target_kind, None);
751    }
752
753    #[test]
754    fn verifier_rejects_partial_v2_policy_claims_and_accepts_full_sets() {
755        let signing_key = crate::keys::SigningKey::generate();
756        let verifying_key = signing_key.verifying_key();
757        let signer = TokenSigner::new(signing_key, "test-issuer");
758        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
759
760        // A signed token with only a subset of the v2 identity fields fails
761        // verification at the authorization boundary.
762        let partial = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
763            .with_metering_key("account:42")
764            .with_account_key("account:42")
765            .build();
766        let token = signer.sign(partial).unwrap();
767        assert!(matches!(
768            verifier.verify(&token, None, None),
769            Err(VerifyError::InvalidPolicyClaims(_))
770        ));
771
772        // The complete authenticated tuple verifies and resolves.
773        let full = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
774            .with_metering_key("account:42")
775            .with_plan("pro")
776            .with_actor_key("user:1")
777            .with_account_key("account:42")
778            .with_consumer_key("consumer:abc123")
779            .with_policy_version(2)
780            .with_account_limits(Limits::default())
781            .build();
782        let token = signer.sign(full).unwrap();
783        let context = verifier.verify(&token, None, None).unwrap();
784        assert!(!context.is_legacy_policy());
785        assert_eq!(context.account_key(), "account:42");
786        assert_eq!(context.consumer_key(), "consumer:abc123");
787        assert_eq!(context.policy_version, Some(2));
788    }
789
790    #[test]
791    fn test_expired_token() {
792        let signing_key = crate::keys::SigningKey::generate();
793        let verifying_key = signing_key.verifying_key();
794
795        let signer = TokenSigner::new(signing_key, "test-issuer");
796        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
797
798        // Create expired claims
799        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
800            .with_ttl(0) // Already expired
801            .with_scope("read")
802            .with_metering_key("meter-123")
803            .with_key_class(KeyClass::Publishable)
804            .build();
805
806        let token = signer.sign(claims).unwrap();
807
808        // Should fail with expired error
809        let result = verifier.verify(&token, None, None);
810        assert!(matches!(result, Err(VerifyError::Expired)));
811    }
812
813    #[test]
814    fn test_future_issued_token_is_not_yet_valid() {
815        let signing_key = crate::keys::SigningKey::generate();
816        let verifying_key = signing_key.verifying_key();
817        let signer = TokenSigner::new(signing_key, "test-issuer");
818        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
819        let mut claims = create_test_claims();
820        claims.iat += 300;
821
822        let token = signer.sign(claims).unwrap();
823
824        assert!(matches!(
825            verifier.verify(&token, None, None),
826            Err(VerifyError::NotYetValid)
827        ));
828    }
829
830    #[test]
831    fn test_invalid_signature() {
832        let signing_key = crate::keys::SigningKey::generate();
833        let wrong_signing_key = crate::keys::SigningKey::generate();
834        let wrong_verifying_key = wrong_signing_key.verifying_key();
835
836        let signer = TokenSigner::new(signing_key, "test-issuer");
837        let verifier = TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
838
839        let claims = create_test_claims();
840        let token = signer.sign(claims).unwrap();
841
842        // Should fail with invalid signature
843        let result = verifier.verify(&token, None, None);
844        assert!(matches!(result, Err(VerifyError::InvalidSignature)));
845    }
846
847    #[test]
848    fn test_wrong_issuer() {
849        let signing_key = crate::keys::SigningKey::generate();
850        let verifying_key = signing_key.verifying_key();
851
852        let signer = TokenSigner::new(signing_key, "wrong-issuer");
853        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
854
855        // Create claims with the wrong issuer
856        let claims = SessionClaims::builder("wrong-issuer", "test-subject", "test-audience")
857            .with_ttl(300)
858            .with_scope("read")
859            .with_metering_key("meter-123")
860            .with_key_class(KeyClass::Publishable)
861            .build();
862        let token = signer.sign(claims).unwrap();
863
864        // Should fail with invalid issuer
865        let result = verifier.verify(&token, None, None);
866        assert!(matches!(result, Err(VerifyError::InvalidIssuer)));
867    }
868
869    #[test]
870    #[allow(deprecated)]
871    fn the_single_audience_accessor_still_answers_for_scalar_verifiers() {
872        // Source compatibility: this is what every pre-existing caller uses.
873        let verifying_key = crate::keys::SigningKey::generate().verifying_key();
874        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "deployment-31");
875        assert_eq!(verifier.audience(), "deployment-31");
876        assert_eq!(verifier.audiences().as_single(), Some("deployment-31"));
877    }
878
879    /// One verifier serving several audiences accepts each of them, and the
880    /// *matched* audience tells the caller which one a token was minted for.
881    #[test]
882    fn a_multi_audience_verifier_accepts_each_and_reports_which_matched() {
883        let signing_key = crate::keys::SigningKey::generate();
884        let verifying_key = signing_key.verifying_key();
885        let signer = TokenSigner::new(signing_key, "test-issuer");
886
887        let verifier = TokenVerifier::with_audiences(
888            verifying_key,
889            "test-issuer",
890            ["deployment-31", "deployment-32"],
891        )
892        .expect("non-empty audience set");
893
894        for audience in ["deployment-31", "deployment-32"] {
895            let claims = SessionClaims::builder("test-issuer", "test-subject", audience)
896                .with_ttl(300)
897                .with_scope("read")
898                .with_metering_key("meter-123")
899                .with_key_class(KeyClass::Publishable)
900                .build();
901            let token = signer.sign(claims).unwrap();
902
903            let ctx = verifier
904                .verify(&token, None, None)
905                .unwrap_or_else(|error| panic!("{audience} should verify: {error:?}"));
906            // The caller learns which audience matched.
907            assert_eq!(ctx.audience, audience);
908        }
909
910        // An audience outside the set is still rejected, which is what stops a
911        // validly signed token being accepted where it does not belong.
912        let claims = SessionClaims::builder("test-issuer", "test-subject", "deployment-99")
913            .with_ttl(300)
914            .with_scope("read")
915            .with_metering_key("meter-123")
916            .with_key_class(KeyClass::Publishable)
917            .build();
918        let token = signer.sign(claims).unwrap();
919        assert!(matches!(
920            verifier.verify(&token, None, None),
921            Err(VerifyError::InvalidAudience)
922        ));
923    }
924
925    #[test]
926    fn test_wrong_audience() {
927        let signing_key = crate::keys::SigningKey::generate();
928        let verifying_key = signing_key.verifying_key();
929
930        let signer = TokenSigner::new(signing_key, "test-issuer");
931        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "expected-audience");
932
933        let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
934            .with_ttl(300)
935            .with_scope("read")
936            .with_metering_key("meter-123")
937            .with_key_class(KeyClass::Publishable)
938            .build();
939        let token = signer.sign(claims).unwrap();
940
941        let result = verifier.verify(&token, None, None);
942        assert!(matches!(result, Err(VerifyError::InvalidAudience)));
943    }
944
945    #[test]
946    fn test_origin_mismatch() {
947        let signing_key = crate::keys::SigningKey::generate();
948        let verifying_key = signing_key.verifying_key();
949
950        let signer = TokenSigner::new(signing_key, "test-issuer");
951        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
952            .with_origin_validation();
953
954        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
955            .with_ttl(300)
956            .with_scope("read")
957            .with_metering_key("meter-123")
958            .with_origin("https://allowed.example")
959            .with_key_class(KeyClass::Publishable)
960            .build();
961        let token = signer.sign(claims).unwrap();
962
963        let result = verifier.verify(&token, Some("https://other.example"), None);
964        assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
965    }
966
967    #[test]
968    fn test_origin_validation_success() {
969        let signing_key = crate::keys::SigningKey::generate();
970        let verifying_key = signing_key.verifying_key();
971
972        let signer = TokenSigner::new(signing_key, "test-issuer");
973        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
974            .with_origin_validation();
975
976        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
977            .with_ttl(300)
978            .with_scope("read")
979            .with_metering_key("meter-123")
980            .with_origin("https://allowed.example")
981            .with_key_class(KeyClass::Publishable)
982            .build();
983        let token = signer.sign(claims).unwrap();
984
985        let context = verifier
986            .verify(&token, Some("https://allowed.example"), None)
987            .unwrap();
988        assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
989    }
990
991    #[test]
992    fn test_origin_validation_requires_origin_claim() {
993        let signing_key = crate::keys::SigningKey::generate();
994        let verifying_key = signing_key.verifying_key();
995
996        let signer = TokenSigner::new(signing_key, "test-issuer");
997        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
998            .with_origin_validation();
999
1000        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1001            .with_ttl(300)
1002            .with_scope("read")
1003            .with_metering_key("meter-123")
1004            .with_key_class(KeyClass::Publishable)
1005            .build();
1006        let token = signer.sign(claims).unwrap();
1007
1008        let result = verifier.verify(&token, None, None);
1009        assert!(matches!(
1010            result,
1011            Err(VerifyError::MissingClaim(ref claim)) if claim == "origin"
1012        ));
1013    }
1014
1015    #[test]
1016    fn test_client_ip_validation_requires_client_ip_claim() {
1017        let signing_key = crate::keys::SigningKey::generate();
1018        let verifying_key = signing_key.verifying_key();
1019
1020        let signer = TokenSigner::new(signing_key, "test-issuer");
1021        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1022            .with_client_ip_validation();
1023
1024        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1025            .with_ttl(300)
1026            .with_scope("read")
1027            .with_metering_key("meter-123")
1028            .with_key_class(KeyClass::Publishable)
1029            .build();
1030        let token = signer.sign(claims).unwrap();
1031
1032        let result = verifier.verify(&token, None, None);
1033        assert!(matches!(
1034            result,
1035            Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
1036        ));
1037    }
1038
1039    fn ip_bound_token(signer: &TokenSigner, client_ip: &str) -> String {
1040        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1041            .with_ttl(300)
1042            .with_scope("read")
1043            .with_metering_key("meter-123")
1044            .with_key_class(KeyClass::Publishable)
1045            .with_client_ip(client_ip)
1046            .build();
1047        signer.sign(claims).unwrap()
1048    }
1049
1050    #[test]
1051    fn test_client_ip_validation_fails_closed_when_client_ip_unknown() {
1052        let signing_key = crate::keys::SigningKey::generate();
1053        let verifying_key = signing_key.verifying_key();
1054        let signer = TokenSigner::new(signing_key, "test-issuer");
1055        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1056            .with_client_ip_validation();
1057        let token = ip_bound_token(&signer, "198.51.100.7");
1058
1059        assert!(matches!(
1060            verifier.verify(&token, None, None),
1061            Err(VerifyError::MissingClaim(ref claim)) if claim == "client_ip"
1062        ));
1063        assert!(matches!(
1064            verifier.verify(&token, None, Some("203.0.113.9")),
1065            Err(VerifyError::OriginMismatch { .. })
1066        ));
1067        let context = verifier.verify(&token, None, Some("198.51.100.7")).unwrap();
1068        assert_eq!(context.subject, "test-subject");
1069    }
1070
1071    #[test]
1072    fn test_ip_bound_token_passes_when_client_ip_validation_is_off() {
1073        let signing_key = crate::keys::SigningKey::generate();
1074        let verifying_key = signing_key.verifying_key();
1075        let signer = TokenSigner::new(signing_key, "test-issuer");
1076        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1077        let token = ip_bound_token(&signer, "198.51.100.7");
1078
1079        assert!(verifier.verify(&token, None, None).is_ok());
1080    }
1081
1082    #[test]
1083    fn test_origin_bound_token_allows_no_origin_when_not_required() {
1084        // This tests the non-browser client scenario (Rust, Python, etc.)
1085        // where the client doesn't send an Origin header, but the JWT has
1086        // an origin claim from when the token was minted via browser/API.
1087        // When require_origin is false, the connection should still be allowed
1088        // for defense-in-depth flexibility.
1089        let signing_key = crate::keys::SigningKey::generate();
1090        let verifying_key = signing_key.verifying_key();
1091
1092        let signer = TokenSigner::new(signing_key, "test-issuer");
1093        // Verifier WITHOUT origin validation (the default for public stacks)
1094        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1095
1096        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1097            .with_ttl(300)
1098            .with_scope("read")
1099            .with_metering_key("meter-123")
1100            .with_origin("https://example.com") // Token has origin claim
1101            .with_key_class(KeyClass::Publishable)
1102            .build();
1103        let token = signer.sign(claims).unwrap();
1104
1105        // No origin provided, but require_origin is false - should succeed
1106        let context = verifier.verify(&token, None, None).unwrap();
1107        assert_eq!(context.origin.as_deref(), Some("https://example.com"));
1108    }
1109
1110    #[test]
1111    fn test_origin_bound_token_validates_when_origin_provided_even_when_not_required() {
1112        // When origin IS provided, it should still be validated against the token
1113        // even when require_origin is false (defense-in-depth)
1114        let signing_key = crate::keys::SigningKey::generate();
1115        let verifying_key = signing_key.verifying_key();
1116
1117        let signer = TokenSigner::new(signing_key, "test-issuer");
1118        // Verifier WITHOUT origin validation (the default)
1119        let verifier = TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1120
1121        let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1122            .with_ttl(300)
1123            .with_scope("read")
1124            .with_metering_key("meter-123")
1125            .with_origin("https://allowed.example")
1126            .with_key_class(KeyClass::Publishable)
1127            .build();
1128        let token = signer.sign(claims).unwrap();
1129
1130        // Origin provided and matches - should succeed
1131        let context = verifier
1132            .verify(&token, Some("https://allowed.example"), None)
1133            .unwrap();
1134        assert_eq!(context.origin.as_deref(), Some("https://allowed.example"));
1135
1136        // Origin provided but doesn't match - should fail
1137        let result = verifier.verify(&token, Some("https://evil.example"), None);
1138        assert!(matches!(result, Err(VerifyError::OriginMismatch { .. })));
1139    }
1140}