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}