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