common_access_token/
cat.rs

1//! Core functionality for Common Access Token (CAT) generation and validation.
2//!
3//! This module provides the main entry point for working with CAT tokens. It implements:
4//! - Token generation with HMAC signatures
5//! - Token validation with comprehensive security checks
6//! - Conversion between JSON and CAT claims
7//!
8//! The `Cat` struct is the primary interface for applications using this library.
9//! It handles the cryptographic operations, token format, and claim validation.
10//!
11//! # Examples
12//!
13//! ```
14//! use common_access_token::{Cat, CatGenerateOptions, CatOptions, CatValidationType, Claims};
15//! use std::collections::HashMap;
16//!
17//! // Create a CAT instance with a cryptographic key
18//! let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
19//! let cat = Cat::new(CatOptions {
20//!     keys: HashMap::from([("Symmetric256".to_string(), key)]),
21//!     expect_cwt_tag: true,
22//! });
23//!
24//! // Create claims for the token
25//! let mut claims = Claims::new();
26//! claims.set_issuer("example");
27//! claims.set_subject("user123");
28//! claims.set_audience("service");
29//!
30//! // Generate a token
31//! let token = cat.generate(claims, &CatGenerateOptions {
32//!     validation_type: CatValidationType::Mac,
33//!     alg: "HS256".to_string(),
34//!     kid: "Symmetric256".to_string(),
35//!     generate_cwt_id: true,
36//! }).unwrap();
37//! ```
38
39use crate::claims::{ClaimValue, Claims, LABEL_CTI};
40use crate::cose::{CoseMac0, ALG_HS256, HEADER_ALG, HEADER_KID, TAG_COSE_MAC0, TAG_CWT};
41use crate::error::Error;
42use crate::util::{current_time_secs, from_base64_url, generate_random_hex, to_base64_no_padding};
43use serde_json::json;
44use std::collections::{BTreeMap, HashMap};
45
46/// Type of CAT validation mechanism to use for token generation and validation.
47///
48/// This enum specifies the cryptographic mechanism used to secure the token.
49#[derive(Debug, Clone, PartialEq)]
50pub enum CatValidationType {
51    /// HMAC-based authentication code (symmetric key)
52    Mac,
53
54    /// Digital signature (asymmetric key) - not currently implemented
55    Sign,
56
57    /// No cryptographic protection - not recommended for production use
58    None,
59}
60
61/// Options for validating a CAT token.
62///
63/// This struct contains parameters that control how token validation is performed,
64/// including which issuers and audiences are considered valid.
65///
66/// # Examples
67///
68/// ```
69/// use common_access_token::CatValidationOptions;
70///
71/// // Basic validation requiring a specific issuer
72/// let options = CatValidationOptions {
73///     issuer: "trusted-issuer".to_string(),
74///     audience: None, // Don't validate audience
75/// };
76///
77/// // Validation with audience restriction
78/// let options_with_audience = CatValidationOptions {
79///     issuer: "trusted-issuer".to_string(),
80///     audience: Some(vec!["app-1".to_string(), "app-2".to_string()]),
81/// };
82/// ```
83#[derive(Debug, Clone)]
84pub struct CatValidationOptions {
85    /// Expected issuer of token. The token will be rejected if it doesn't match this value.
86    pub issuer: String,
87
88    /// Allowed audiences for token. If provided, the token must contain at least one
89    /// of these audiences to be considered valid. If None, audience validation is skipped.
90    pub audience: Option<Vec<String>>,
91}
92
93/// Options for generating a CAT token.
94///
95/// This struct contains parameters that control how a token is generated,
96/// including which cryptographic algorithm and key to use.
97///
98/// # Examples
99///
100/// ```
101/// use common_access_token::{CatGenerateOptions, CatValidationType};
102///
103/// // Basic token generation options
104/// let options = CatGenerateOptions {
105///     validation_type: CatValidationType::Mac,
106///     alg: "HS256".to_string(),
107///     kid: "key-1".to_string(),
108///     generate_cwt_id: true,
109/// };
110///
111/// // Using the default options
112/// let default_options = CatGenerateOptions::default();
113/// ```
114#[derive(Debug, Clone)]
115pub struct CatGenerateOptions {
116    /// Type of validation mechanism to use for the token.
117    /// Currently, only `CatValidationType::Mac` is fully implemented.
118    pub validation_type: CatValidationType,
119
120    /// Algorithm to use for token generation.
121    /// For `CatValidationType::Mac`, this should be "HS256".
122    pub alg: String,
123
124    /// Key ID to use for token generation. This must match a key ID in the
125    /// `keys` map provided to the `Cat` instance.
126    pub kid: String,
127
128    /// Whether to generate a CWT ID for the token.
129    /// A CWT ID is a unique identifier for the token that can be used for tracking
130    /// or revocation purposes.
131    pub generate_cwt_id: bool,
132}
133
134impl Default for CatGenerateOptions {
135    /// Creates a default set of options for token generation.
136    ///
137    /// The default configuration:
138    /// - Uses HMAC-based authentication (`CatValidationType::Mac`)
139    /// - Uses the HS256 algorithm (HMAC with SHA-256)
140    /// - Has an empty key ID (must be set before use)
141    /// - Does not generate a CWT ID
142    fn default() -> Self {
143        CatGenerateOptions {
144            validation_type: CatValidationType::Mac,
145            alg: "HS256".to_string(),
146            kid: "".to_string(),
147            generate_cwt_id: false,
148        }
149    }
150}
151
152/// Configuration options for creating a CAT instance.
153///
154/// This struct contains the cryptographic keys and format options used
155/// for token generation and validation.
156///
157/// # Examples
158///
159/// ```
160/// use common_access_token::CatOptions;
161/// use std::collections::HashMap;
162///
163/// // Create options with a single key
164/// let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
165/// let options = CatOptions {
166///     keys: HashMap::from([("key-1".to_string(), key)]),
167///     expect_cwt_tag: true,
168/// };
169///
170/// // Create empty options (not useful until keys are added)
171/// let empty_options = CatOptions::default();
172/// ```
173#[derive(Debug, Clone, Default)]
174pub struct CatOptions {
175    /// Map of key IDs to cryptographic keys.
176    ///
177    /// Each entry maps a key identifier (kid) to the actual key material (as bytes).
178    /// When generating or validating tokens, the key ID is used to look up the
179    /// appropriate key in this map.
180    pub keys: HashMap<String, Vec<u8>>,
181
182    /// Whether tokens should include the CWT tag.
183    ///
184    /// When true, tokens will be wrapped with the CWT tag (61) as defined in RFC 8392.
185    /// This should be set to true for interoperability with other implementations.
186    pub expect_cwt_tag: bool,
187}
188
189/// Common Access Token (CAT) validator and generator.
190///
191/// This is the main struct for working with CAT tokens. It provides methods for:
192/// - Generating tokens from claims
193/// - Validating tokens and extracting claims
194/// - Converting between JSON and CAT claims
195///
196/// # Examples
197///
198/// ```
199/// use common_access_token::{Cat, CatOptions, CatGenerateOptions, CatValidationOptions, CatValidationType, Claims};
200/// use std::collections::HashMap;
201///
202/// // Create a CAT instance
203/// let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
204/// let cat = Cat::new(CatOptions {
205///     keys: HashMap::from([("key-1".to_string(), key)]),
206///     expect_cwt_tag: true,
207/// });
208///
209/// // Create and generate a token
210/// let mut claims = Claims::new();
211/// claims.set_issuer("example");
212/// let token = cat.generate(claims, &CatGenerateOptions {
213///     validation_type: CatValidationType::Mac,
214///     alg: "HS256".to_string(),
215///     kid: "key-1".to_string(),
216///     generate_cwt_id: true,
217/// }).unwrap();
218///
219/// // Validate the token
220/// let validation_options = CatValidationOptions {
221///     issuer: "example".to_string(),
222///     audience: None,
223/// };
224/// let validated_claims = cat.validate(&token, CatValidationType::Mac, &validation_options).unwrap();
225/// ```
226#[derive(Debug, Clone)]
227pub struct Cat {
228    /// Map of key IDs to cryptographic keys
229    keys: HashMap<String, Vec<u8>>,
230    /// Whether tokens should include the CWT tag
231    expect_cwt_tag: bool,
232}
233
234impl Cat {
235    /// Creates a new CAT instance with the specified options.
236    ///
237    /// # Arguments
238    ///
239    /// * `opts` - Configuration options including cryptographic keys and format settings
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use common_access_token::{Cat, CatOptions};
245    /// use std::collections::HashMap;
246    ///
247    /// let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
248    /// let cat = Cat::new(CatOptions {
249    ///     keys: HashMap::from([("key-1".to_string(), key)]),
250    ///     expect_cwt_tag: true,
251    /// });
252    /// ```
253    pub fn new(opts: CatOptions) -> Self {
254        Cat {
255            keys: opts.keys,
256            expect_cwt_tag: opts.expect_cwt_tag,
257        }
258    }
259
260    /// Validates a CAT token and returns the claims if valid.
261    ///
262    /// This method performs several validation steps:
263    /// 1. Decodes the token from base64
264    /// 2. Verifies the cryptographic signature/MAC
265    /// 3. Validates the claims (issuer, expiration, audience)
266    ///
267    /// # Arguments
268    ///
269    /// * `token` - The CAT token as a base64url-encoded string
270    /// * `validation_type` - The type of validation to perform (Mac, Sign, or None)
271    /// * `opts` - Options controlling validation behavior
272    ///
273    /// # Returns
274    ///
275    /// * `Ok(Claims)` - The validated claims from the token
276    /// * `Err(Error)` - An error indicating why validation failed
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// # use common_access_token::{Cat, CatOptions, CatValidationOptions, CatValidationType};
282    /// # use std::collections::HashMap;
283    /// #
284    /// # let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
285    /// # let cat = Cat::new(CatOptions {
286    /// #     keys: HashMap::from([("key-1".to_string(), key)]),
287    /// #     expect_cwt_tag: true,
288    /// # });
289    /// #
290    /// // Validate a token
291    /// let token = "..."; // Base64url-encoded token
292    /// let validation_options = CatValidationOptions {
293    ///     issuer: "trusted-issuer".to_string(),
294    ///     audience: Some(vec!["my-app".to_string()]),
295    /// };
296    ///
297    /// match cat.validate(token, CatValidationType::Mac, &validation_options) {
298    ///     Ok(claims) => {
299    ///         println!("Token is valid!");
300    ///         println!("Subject: {:?}", claims.get_subject());
301    ///     },
302    ///     Err(err) => {
303    ///         println!("Token validation failed: {}", err);
304    ///     }
305    /// }
306    /// ```
307    pub fn validate(
308        &self,
309        token: &str,
310        validation_type: CatValidationType,
311        opts: &CatValidationOptions,
312    ) -> Result<Claims, Error> {
313        let token_without_padding = token;
314        let token_bytes = from_base64_url(token_without_padding)?;
315
316        match validation_type {
317            CatValidationType::Mac => {
318                if self.keys.is_empty() {
319                    return Err(Error::MissingOptionsForMacValidation);
320                }
321
322                let mut last_error = Error::KeyNotFound("No keys available".to_string());
323
324                // Try each key until one works
325                for (kid, key) in &self.keys {
326                    match self.validate_mac(&token_bytes, key, kid) {
327                        Ok(claims) => {
328                            // Validate the claims
329                            self.validate_claims(&claims, opts)?;
330                            return Ok(claims);
331                        }
332                        Err(err) => {
333                            last_error = err;
334                        }
335                    }
336                }
337
338                Err(last_error)
339            }
340            _ => Err(Error::UnsupportedValidationType),
341        }
342    }
343
344    /// Generates a CAT token from the provided claims.
345    ///
346    /// This method creates a token by:
347    /// 1. Optionally generating a CWT ID if requested
348    /// 2. Serializing the claims to CBOR
349    /// 3. Creating a cryptographic signature/MAC
350    /// 4. Encoding the result as a base64url string
351    ///
352    /// # Arguments
353    ///
354    /// * `claims` - The claims to include in the token
355    /// * `opts` - Options controlling token generation
356    ///
357    /// # Returns
358    ///
359    /// * `Ok(String)` - The generated token as a base64url-encoded string
360    /// * `Err(Error)` - An error indicating why token generation failed
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// # use common_access_token::{Cat, CatOptions, CatGenerateOptions, CatValidationType, Claims};
366    /// # use std::collections::HashMap;
367    /// #
368    /// # let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
369    /// # let cat = Cat::new(CatOptions {
370    /// #     keys: HashMap::from([("key-1".to_string(), key)]),
371    /// #     expect_cwt_tag: true,
372    /// # });
373    /// #
374    /// // Create claims
375    /// let mut claims = Claims::new();
376    /// claims.set_issuer("my-service");
377    /// claims.set_subject("user-123");
378    /// claims.set_audience("client-app");
379    ///
380    /// // Set expiration time
381    /// let now = std::time::SystemTime::now()
382    ///     .duration_since(std::time::UNIX_EPOCH)
383    ///     .unwrap()
384    ///     .as_secs() as i64;
385    /// claims.set_expiration(now + 3600); // 1 hour from now
386    ///
387    /// // Generate the token
388    /// let token = cat.generate(claims, &CatGenerateOptions {
389    ///     validation_type: CatValidationType::Mac,
390    ///     alg: "HS256".to_string(),
391    ///     kid: "key-1".to_string(),
392    ///     generate_cwt_id: true,
393    /// }).unwrap();
394    ///
395    /// println!("Generated token: {}", token);
396    /// ```
397    pub fn generate(&self, claims: Claims, opts: &CatGenerateOptions) -> Result<String, Error> {
398        let mut claims = claims;
399
400        // Generate a CWT ID if requested
401        if opts.generate_cwt_id && claims.get_claim(LABEL_CTI).is_none() {
402            let cti = generate_random_hex(16);
403            claims.set_cwt_id(cti.as_bytes());
404        }
405
406        match opts.validation_type {
407            CatValidationType::Mac => {
408                let key = self
409                    .keys
410                    .get(&opts.kid)
411                    .ok_or_else(|| Error::KeyNotFound(opts.kid.clone()))?;
412
413                let token_bytes = self.create_mac(&claims, key, &opts.kid)?;
414                Ok(to_base64_no_padding(&token_bytes))
415            }
416            _ => Err(Error::UnsupportedValidationType),
417        }
418    }
419
420    /// Generates a CAT token from a JSON object containing claims.
421    ///
422    /// This is a convenience method that converts JSON claims to a `Claims` object
423    /// and then generates a token. It's useful when working with JSON data from
424    /// external sources.
425    ///
426    /// # Arguments
427    ///
428    /// * `json_claims` - JSON object containing claims (e.g., `{"iss": "issuer", "sub": "subject"}`)
429    /// * `opts` - Options controlling token generation
430    ///
431    /// # Returns
432    ///
433    /// * `Ok(String)` - The generated token as a base64url-encoded string
434    /// * `Err(Error)` - An error indicating why token generation failed
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// # use common_access_token::{Cat, CatOptions, CatGenerateOptions, CatValidationType};
440    /// # use std::collections::HashMap;
441    /// # use serde_json::json;
442    /// #
443    /// # let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
444    /// # let cat = Cat::new(CatOptions {
445    /// #     keys: HashMap::from([("key-1".to_string(), key)]),
446    /// #     expect_cwt_tag: true,
447    /// # });
448    /// #
449    /// // Create claims as JSON
450    /// let now = std::time::SystemTime::now()
451    ///     .duration_since(std::time::UNIX_EPOCH)
452    ///     .unwrap()
453    ///     .as_secs() as i64;
454    ///
455    /// let json_claims = json!({
456    ///     "iss": "my-service",
457    ///     "sub": "user-123",
458    ///     "aud": "client-app",
459    ///     "exp": now + 3600, // 1 hour from now
460    ///     "iat": now
461    /// });
462    ///
463    /// // Generate the token
464    /// let token = cat.generate_from_json(json_claims, &CatGenerateOptions {
465    ///     validation_type: CatValidationType::Mac,
466    ///     alg: "HS256".to_string(),
467    ///     kid: "key-1".to_string(),
468    ///     generate_cwt_id: true,
469    /// }).unwrap();
470    /// ```
471    pub fn generate_from_json(
472        &self,
473        json_claims: serde_json::Value,
474        opts: &CatGenerateOptions,
475    ) -> Result<String, Error> {
476        let claims = self.json_to_claims(json_claims, opts)?;
477        self.generate(claims, opts)
478    }
479
480    // Private methods
481
482    /// Validates a token using HMAC-based authentication.
483    ///
484    /// This method handles the cryptographic verification of the token and
485    /// extracts the claims if the verification succeeds.
486    ///
487    /// # Arguments
488    ///
489    /// * `token_bytes` - The raw binary token data
490    /// * `key` - The cryptographic key to use for verification
491    /// * `_kid` - The key ID (not used in the verification process)
492    ///
493    /// # Returns
494    ///
495    /// * `Ok(Claims)` - The claims from the token if verification succeeds
496    /// * `Err(Error)` - An error if verification fails
497    fn validate_mac(&self, token_bytes: &[u8], key: &[u8], _kid: &str) -> Result<Claims, Error> {
498        // Note: _kid parameter is not used in the validation process, as we extract the kid from the token if needed
499        // Check if the token has a CWT tag
500        if self.expect_cwt_tag {
501            // Try to parse the token as a tagged CWT
502            match ciborium::de::from_reader::<(u64, Vec<u8>), _>(token_bytes) {
503                Ok((tag, inner_data)) => {
504                    if tag != TAG_CWT {
505                        return Err(Error::ExpectedCwtTag);
506                    }
507
508                    // Try to parse the inner data as a tagged COSE_Mac0
509                    match ciborium::de::from_reader::<(u64, Vec<u8>), _>(&inner_data[..]) {
510                        Ok((inner_tag, cose_data)) => {
511                            if inner_tag != TAG_COSE_MAC0 {
512                                return Err(Error::TagMismatch);
513                            }
514
515                            // Parse the COSE_Mac0 structure
516                            let cose_mac0 = CoseMac0::from_cbor(&cose_data)?;
517
518                            // Verify the MAC
519                            cose_mac0.verify(key)?;
520
521                            // Parse the payload
522                            let payload = cose_mac0.get_payload();
523                            let claims_map: BTreeMap<u64, ClaimValue> =
524                                ciborium::de::from_reader(payload)?;
525
526                            Ok(Claims::from_map(claims_map))
527                        }
528                        Err(_) => {
529                            // Try direct parsing as a fallback
530                            let cose_mac0 = CoseMac0::from_cbor(&inner_data)?;
531                            cose_mac0.verify(key)?;
532                            let claims_map: BTreeMap<u64, ClaimValue> =
533                                ciborium::de::from_reader(cose_mac0.get_payload())?;
534                            Ok(Claims::from_map(claims_map))
535                        }
536                    }
537                }
538                Err(_) => {
539                    // If we can't parse it as a tagged CWT, try parsing it directly as a COSE_Mac0
540                    let cose_mac0 = CoseMac0::from_cbor(token_bytes)?;
541                    cose_mac0.verify(key)?;
542                    let claims_map: BTreeMap<u64, ClaimValue> =
543                        ciborium::de::from_reader(cose_mac0.get_payload())?;
544                    Ok(Claims::from_map(claims_map))
545                }
546            }
547        } else {
548            // Parse the token directly as a COSE_Mac0 structure
549            let cose_mac0 = CoseMac0::from_cbor(token_bytes)?;
550
551            // Verify the MAC
552            cose_mac0.verify(key)?;
553
554            // Parse the payload
555            let payload = cose_mac0.get_payload();
556            let claims_map: BTreeMap<u64, ClaimValue> = ciborium::de::from_reader(payload)?;
557
558            Ok(Claims::from_map(claims_map))
559        }
560    }
561
562    /// Creates a MAC (Message Authentication Code) for the given claims.
563    ///
564    /// This method handles the cryptographic signing of the token:
565    /// 1. Serializes the claims to CBOR
566    /// 2. Creates the COSE_Mac0 structure
567    /// 3. Computes the HMAC
568    /// 4. Wraps the result with appropriate tags
569    ///
570    /// # Arguments
571    ///
572    /// * `claims` - The claims to include in the token
573    /// * `key` - The cryptographic key to use for signing
574    /// * `kid` - The key ID to include in the token header
575    ///
576    /// # Returns
577    ///
578    /// * `Ok(Vec<u8>)` - The raw binary token data
579    /// * `Err(Error)` - An error if token creation fails
580    fn create_mac(&self, claims: &Claims, key: &[u8], kid: &str) -> Result<Vec<u8>, Error> {
581        // Serialize the claims to CBOR
582        let mut claims_cbor = Vec::new();
583        ciborium::ser::into_writer(claims.as_map(), &mut claims_cbor)?;
584
585        // Create the protected header
586        let mut protected_header = BTreeMap::new();
587        protected_header.insert(HEADER_ALG, json!(ALG_HS256));
588
589        // Create the unprotected header with the key ID as binary
590        let mut unprotected_header = BTreeMap::new();
591        // Store the kid as binary value (bytes) instead of a string
592        unprotected_header.insert(
593            HEADER_KID,
594            serde_json::Value::Array(
595                kid.as_bytes()
596                    .iter()
597                    .map(|&b| serde_json::Value::Number(serde_json::Number::from(b)))
598                    .collect(),
599            ),
600        );
601
602        // Create the COSE_Mac0 structure
603        let mut cose_mac0 = CoseMac0::new(protected_header, unprotected_header, claims_cbor);
604
605        // Create the MAC tag
606        cose_mac0.create_tag(key)?;
607
608        // Serialize the COSE_Mac0 structure to CBOR
609        let cose_mac0_cbor = cose_mac0.to_cbor()?;
610
611        if self.expect_cwt_tag {
612            // Wrap the COSE_Mac0 in a CWT tag
613            let mut result = Vec::new();
614
615            // First tag with COSE_Mac0 tag
616            let tagged_cose_mac0 = (TAG_COSE_MAC0, cose_mac0_cbor);
617            let mut tagged_cose_mac0_cbor = Vec::new();
618            ciborium::ser::into_writer(&tagged_cose_mac0, &mut tagged_cose_mac0_cbor)?;
619
620            // Then tag with CWT tag
621            let cwt = (TAG_CWT, tagged_cose_mac0_cbor);
622            ciborium::ser::into_writer(&cwt, &mut result)?;
623
624            Ok(result)
625        } else {
626            Ok(cose_mac0_cbor)
627        }
628    }
629
630    /// Validates the claims in a token against the provided validation options.
631    ///
632    /// This method performs several validation checks:
633    /// 1. Verifies the issuer matches the expected issuer
634    /// 2. Checks if the token has expired
635    /// 3. Checks if the token is not yet active (if not-before claim is present)
636    /// 4. Verifies the audience is in the list of allowed audiences (if specified)
637    ///
638    /// # Arguments
639    ///
640    /// * `claims` - The claims to validate
641    /// * `opts` - The validation options specifying expected values
642    ///
643    /// # Returns
644    ///
645    /// * `Ok(())` - If all validation checks pass
646    /// * `Err(Error)` - An error indicating which validation check failed
647    fn validate_claims(&self, claims: &Claims, opts: &CatValidationOptions) -> Result<(), Error> {
648        // Check issuer
649        if let Some(issuer) = claims.get_issuer() {
650            if issuer != &opts.issuer {
651                return Err(Error::InvalidIssuer {
652                    expected: opts.issuer.clone(),
653                    actual: issuer.clone(),
654                });
655            }
656        }
657
658        // Check expiration
659        if let Some(exp) = claims.get_expiration() {
660            let now = current_time_secs();
661            if exp < now {
662                return Err(Error::TokenExpired);
663            }
664        }
665
666        // Check not before
667        if let Some(nbf) = claims.get_not_before() {
668            let now = current_time_secs();
669            if nbf > now {
670                return Err(Error::TokenNotActive);
671            }
672        }
673
674        // Check audience
675        if let Some(ref expected_audiences) = opts.audience {
676            if let Some(token_audiences) = claims.get_audience() {
677                if !expected_audiences
678                    .iter()
679                    .any(|expected| token_audiences.contains(expected))
680                {
681                    return Err(Error::InvalidAudience(token_audiences));
682                }
683            }
684        }
685
686        Ok(())
687    }
688
689    /// Converts a JSON object to a Claims object.
690    ///
691    /// This method maps standard JWT claim names to their CWT equivalents:
692    /// - "iss" -> issuer
693    /// - "sub" -> subject
694    /// - "aud" -> audience
695    /// - "exp" -> expiration
696    /// - "nbf" -> not before
697    /// - "iat" -> issued at
698    /// - "cti" -> CWT ID
699    ///
700    /// # Arguments
701    ///
702    /// * `json_claims` - JSON object containing claims
703    /// * `opts` - Generation options (used for CWT ID generation)
704    ///
705    /// # Returns
706    ///
707    /// * `Ok(Claims)` - The converted claims
708    /// * `Err(Error)` - An error if conversion fails
709    ///   Re-signs an existing token with a new expiration time.
710    ///
711    /// This method extends the lifetime of an existing token by:
712    ///
713    /// 1. Validating the existing token
714    /// 2. Extracting the claims
715    /// 3. Updating the expiration time
716    /// 4. Generating a new token with the updated claims
717    ///
718    /// # Arguments
719    ///
720    /// * `token` - The existing token to re-sign
721    /// * `validation_opts` - Options for validating the existing token
722    /// * `new_expiration` - The new expiration time (in seconds since UNIX epoch)
723    /// * `generate_opts` - Options for generating the new token
724    ///
725    /// # Returns
726    ///
727    /// * `Ok(String)` - The newly generated token with extended lifetime
728    /// * `Err(Error)` - An error if token validation or generation fails
729    ///
730    /// # Examples
731    ///
732    /// ```
733    /// # use common_access_token::{Cat, CatOptions, CatGenerateOptions, CatValidationOptions, CatValidationType, Claims};
734    /// # use std::collections::HashMap;
735    /// # use std::time::SystemTime;
736    /// #
737    /// # // Create a CAT instance for testing
738    /// # let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388").unwrap();
739    /// # let cat = Cat::new(CatOptions {
740    /// #     keys: HashMap::from([("key-1".to_string(), key)]),
741    /// #     expect_cwt_tag: true,
742    /// # });
743    /// #
744    /// # // First, create a valid token to work with
745    /// # let mut claims = Claims::new();
746    /// # claims.set_issuer("trusted-issuer");
747    /// # claims.set_subject("user123");
748    /// # let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
749    /// # claims.set_expiration(now + 60); // 1 minute expiration
750    /// # claims.set_issued_at(now);
751    /// #
752    /// # let generate_options = CatGenerateOptions {
753    /// #     validation_type: CatValidationType::Mac,
754    /// #     alg: "HS256".to_string(),
755    /// #     kid: "key-1".to_string(),
756    /// #     generate_cwt_id: true,
757    /// # };
758    /// #
759    /// # // Generate a token that we'll use as our "existing token"
760    /// # let existing_token = cat.generate(claims, &generate_options).unwrap();
761    /// #
762    /// // Validation options for the existing token
763    /// let validation_options = CatValidationOptions {
764    ///     issuer: "trusted-issuer".to_string(),
765    ///     audience: None,
766    /// };
767    ///
768    /// // Calculate new expiration time (e.g., 1 hour from now)
769    /// let now = std::time::SystemTime::now()
770    ///     .duration_since(std::time::UNIX_EPOCH)
771    ///     .unwrap()
772    ///     .as_secs() as i64;
773    /// let new_expiration = now + 3600;
774    ///
775    /// // Generation options for the new token
776    /// let generate_options = CatGenerateOptions {
777    ///     validation_type: CatValidationType::Mac,
778    ///     alg: "HS256".to_string(),
779    ///     kid: "key-1".to_string(),
780    ///     generate_cwt_id: true, // Generate a new CWT ID
781    /// };
782    ///
783    /// // Re-sign the token with the new expiration
784    /// let new_token = cat.resign_token(
785    ///     &existing_token,
786    ///     &validation_options,
787    ///     new_expiration,
788    ///     &generate_options
789    /// ).unwrap();
790    /// ```
791    pub fn resign_token(
792        &self,
793        token: &str,
794        validation_opts: &CatValidationOptions,
795        new_expiration: i64,
796        generate_opts: &CatGenerateOptions,
797    ) -> Result<String, Error> {
798        // Validate the existing token
799        let mut claims = self.validate(token, CatValidationType::Mac, validation_opts)?;
800
801        // Update the expiration time
802        claims.set_expiration(new_expiration);
803
804        // Update the issued_at time to current time
805        claims.set_issued_at(current_time_secs());
806
807        // Generate a new token with the updated claims
808        self.generate(claims, generate_opts)
809    }
810
811    fn json_to_claims(
812        &self,
813        json_claims: serde_json::Value,
814        opts: &CatGenerateOptions,
815    ) -> Result<Claims, Error> {
816        let mut claims = Claims::new();
817
818        if let serde_json::Value::Object(map) = json_claims {
819            for (key, value) in map {
820                match key.as_str() {
821                    "iss" => {
822                        if let serde_json::Value::String(s) = value {
823                            claims.set_issuer(&s);
824                        }
825                    }
826                    "sub" => {
827                        if let serde_json::Value::String(s) = value {
828                            claims.set_subject(&s);
829                        }
830                    }
831                    "aud" => match value {
832                        serde_json::Value::String(s) => {
833                            claims.set_audience(&s);
834                        }
835                        serde_json::Value::Array(arr) => {
836                            let audience: Vec<String> = arr
837                                .iter()
838                                .filter_map(|v| {
839                                    if let serde_json::Value::String(s) = v {
840                                        Some(s.clone())
841                                    } else {
842                                        None
843                                    }
844                                })
845                                .collect();
846                            claims.set_audience_list(audience);
847                        }
848                        _ => {}
849                    },
850                    "exp" => {
851                        if let serde_json::Value::Number(n) = value {
852                            if let Some(i) = n.as_i64() {
853                                claims.set_expiration(i);
854                            }
855                        }
856                    }
857                    "nbf" => {
858                        if let serde_json::Value::Number(n) = value {
859                            if let Some(i) = n.as_i64() {
860                                claims.set_not_before(i);
861                            }
862                        }
863                    }
864                    "iat" => {
865                        if let serde_json::Value::Number(n) = value {
866                            if let Some(i) = n.as_i64() {
867                                claims.set_issued_at(i);
868                            }
869                        }
870                    }
871                    "cti" => {
872                        if let serde_json::Value::String(s) = value {
873                            claims.set_cwt_id(s.as_bytes());
874                        }
875                    }
876                    // Add more claim types as needed
877                    _ => {
878                        // For now, we'll ignore other claims
879                    }
880                }
881            }
882        }
883
884        // Generate a CWT ID if requested and not already set
885        if opts.generate_cwt_id && claims.get_claim(LABEL_CTI).is_none() {
886            let cti = generate_random_hex(16);
887            claims.set_cwt_id(cti.as_bytes());
888        }
889
890        Ok(claims)
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::util::current_time_secs;
898    use std::collections::HashMap;
899
900    fn create_test_cat() -> Cat {
901        let key = hex::decode("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388")
902            .unwrap();
903        let mut keys = HashMap::new();
904        keys.insert("Symmetric256".to_string(), key);
905
906        let cat_options = CatOptions {
907            keys,
908            expect_cwt_tag: true,
909        };
910
911        Cat::new(cat_options)
912    }
913
914    #[test]
915    fn test_generate_and_validate() {
916        let cat = create_test_cat();
917
918        // Create claims
919        let mut claims = Claims::new();
920        claims.set_issuer("eyevinn");
921        claims.set_subject("jonas");
922        claims.set_audience("one");
923
924        let now = current_time_secs();
925        claims.set_expiration(now + 120);
926        claims.set_issued_at(now);
927
928        // Generate token
929        let generate_options = CatGenerateOptions {
930            validation_type: CatValidationType::Mac,
931            alg: "HS256".to_string(),
932            kid: "Symmetric256".to_string(),
933            generate_cwt_id: true,
934        };
935
936        let token = cat.generate(claims, &generate_options).unwrap();
937
938        // Validate token
939        let validation_options = CatValidationOptions {
940            issuer: "eyevinn".to_string(),
941            audience: None,
942        };
943
944        let validated_claims = cat
945            .validate(&token, CatValidationType::Mac, &validation_options)
946            .unwrap();
947
948        assert_eq!(validated_claims.get_issuer().unwrap(), "eyevinn");
949        assert_eq!(validated_claims.get_subject().unwrap(), "jonas");
950        assert_eq!(validated_claims.get_audience().unwrap(), vec!["one"]);
951        assert!(validated_claims.get_cwt_id().is_some());
952    }
953
954    #[test]
955    fn test_generate_from_json() {
956        let cat = create_test_cat();
957
958        let now = current_time_secs();
959        let json_claims = json!({
960            "iss": "eyevinn",
961            "sub": "jonas",
962            "aud": "one",
963            "exp": now + 120,
964            "iat": now
965        });
966
967        // Generate token
968        let generate_options = CatGenerateOptions {
969            validation_type: CatValidationType::Mac,
970            alg: "HS256".to_string(),
971            kid: "Symmetric256".to_string(),
972            generate_cwt_id: true,
973        };
974
975        let token = cat
976            .generate_from_json(json_claims, &generate_options)
977            .unwrap();
978
979        // Validate token
980        let validation_options = CatValidationOptions {
981            issuer: "eyevinn".to_string(),
982            audience: None,
983        };
984
985        let validated_claims = cat
986            .validate(&token, CatValidationType::Mac, &validation_options)
987            .unwrap();
988
989        assert_eq!(validated_claims.get_issuer().unwrap(), "eyevinn");
990        assert_eq!(validated_claims.get_subject().unwrap(), "jonas");
991        assert_eq!(validated_claims.get_audience().unwrap(), vec!["one"]);
992    }
993
994    #[test]
995    fn test_token_expiration() {
996        let cat = create_test_cat();
997
998        // Create claims with expired token
999        let mut claims = Claims::new();
1000        claims.set_issuer("eyevinn");
1001        claims.set_expiration(current_time_secs() - 60); // 1 minute in the past
1002
1003        // Generate token
1004        let generate_options = CatGenerateOptions {
1005            validation_type: CatValidationType::Mac,
1006            alg: "HS256".to_string(),
1007            kid: "Symmetric256".to_string(),
1008            generate_cwt_id: false,
1009        };
1010
1011        let token = cat.generate(claims, &generate_options).unwrap();
1012
1013        // Validate token
1014        let validation_options = CatValidationOptions {
1015            issuer: "eyevinn".to_string(),
1016            audience: None,
1017        };
1018
1019        let result = cat.validate(&token, CatValidationType::Mac, &validation_options);
1020        assert!(result.is_err());
1021
1022        match result {
1023            Err(Error::TokenExpired) => {} // Expected error
1024            _ => panic!("Expected TokenExpired error"),
1025        }
1026    }
1027
1028    #[test]
1029    fn test_invalid_issuer() {
1030        let cat = create_test_cat();
1031
1032        // Create claims
1033        let mut claims = Claims::new();
1034        claims.set_issuer("eyevinn");
1035        claims.set_expiration(current_time_secs() + 120);
1036
1037        // Generate token
1038        let generate_options = CatGenerateOptions {
1039            validation_type: CatValidationType::Mac,
1040            alg: "HS256".to_string(),
1041            kid: "Symmetric256".to_string(),
1042            generate_cwt_id: false,
1043        };
1044
1045        let token = cat.generate(claims, &generate_options).unwrap();
1046
1047        // Validate token with wrong issuer
1048        let validation_options = CatValidationOptions {
1049            issuer: "wrong-issuer".to_string(),
1050            audience: None,
1051        };
1052
1053        let result = cat.validate(&token, CatValidationType::Mac, &validation_options);
1054        assert!(result.is_err());
1055
1056        match result {
1057            Err(Error::InvalidIssuer { .. }) => {} // Expected error
1058            _ => panic!("Expected InvalidIssuer error"),
1059        }
1060    }
1061
1062    #[test]
1063    fn test_resign_token() {
1064        let cat = create_test_cat();
1065
1066        // Create claims with a short expiration time
1067        let mut claims = Claims::new();
1068        claims.set_issuer("eyevinn");
1069        claims.set_subject("user123");
1070
1071        let now = current_time_secs();
1072        let initial_exp = now + 60; // 1 minute from now
1073        claims.set_expiration(initial_exp);
1074        claims.set_issued_at(now);
1075
1076        // Generate the initial token
1077        let generate_options = CatGenerateOptions {
1078            validation_type: CatValidationType::Mac,
1079            alg: "HS256".to_string(),
1080            kid: "Symmetric256".to_string(),
1081            generate_cwt_id: true,
1082        };
1083
1084        let token = cat.generate(claims, &generate_options).unwrap();
1085
1086        // Validation options for the token
1087        let validation_options = CatValidationOptions {
1088            issuer: "eyevinn".to_string(),
1089            audience: None,
1090        };
1091
1092        // Verify the initial token
1093        let validated_claims = cat
1094            .validate(&token, CatValidationType::Mac, &validation_options)
1095            .unwrap();
1096        assert_eq!(validated_claims.get_expiration().unwrap(), initial_exp);
1097
1098        // Re-sign the token with a longer expiration
1099        let new_exp = now + 3600; // 1 hour from now
1100        let new_token = cat
1101            .resign_token(&token, &validation_options, new_exp, &generate_options)
1102            .unwrap();
1103
1104        // Verify the new token
1105        let new_validated_claims = cat
1106            .validate(&new_token, CatValidationType::Mac, &validation_options)
1107            .unwrap();
1108
1109        // Check that the expiration time was updated
1110        assert_eq!(new_validated_claims.get_expiration().unwrap(), new_exp);
1111
1112        // Check that other claims were preserved
1113        assert_eq!(new_validated_claims.get_issuer().unwrap(), "eyevinn");
1114        assert_eq!(new_validated_claims.get_subject().unwrap(), "user123");
1115
1116        // Check that issued_at was updated
1117        assert!(new_validated_claims.get_issued_at().unwrap() >= now);
1118
1119        // Verify that the tokens are different
1120        assert_ne!(token, new_token);
1121    }
1122}