common_access_token/token.rs
1//! Token implementation for Common Access Token
2
3use crate::claims::{Claims, RegisteredClaims};
4use crate::error::Error;
5use crate::header::{Algorithm, CborValue, Header, HeaderMap, KeyId};
6use crate::utils::{compute_hmac_sha256, current_timestamp, verify_hmac_sha256};
7use minicbor::{Decoder, Encoder};
8use std::collections::BTreeMap;
9use std::ffi::OsStr;
10use std::path::Path;
11
12/// Common Access Token structure
13#[derive(Debug, Clone)]
14pub struct Token {
15 /// Token header
16 pub header: Header,
17 /// Token claims
18 pub claims: Claims,
19 /// Token signature
20 pub signature: Vec<u8>,
21 /// Original payload bytes (for verification)
22 original_payload_bytes: Option<Vec<u8>>,
23}
24
25impl Token {
26 /// Create a new token with the given header, claims, and signature
27 pub fn new(header: Header, claims: Claims, signature: Vec<u8>) -> Self {
28 Self {
29 header,
30 claims,
31 signature,
32 original_payload_bytes: None,
33 }
34 }
35
36 /// Encode the token to CBOR bytes
37 pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
38 let mut buf = Vec::new();
39 let mut enc = Encoder::new(&mut buf);
40
41 // For HMAC algorithms, use COSE_Mac0 format with CWT tag
42 if let Some(Algorithm::HmacSha256) = self.header.algorithm() {
43 // Apply CWT tag (61)
44 enc.tag(minicbor::data::Tag::new(61))?;
45 // Apply COSE_Mac0 tag (17)
46 enc.tag(minicbor::data::Tag::new(17))?;
47 }
48
49 // COSE structure array with 4 items
50 enc.array(4)?;
51
52 // 1. Protected header (encoded as CBOR and then as bstr)
53 let protected_bytes = encode_map(&self.header.protected)?;
54 enc.bytes(&protected_bytes)?;
55
56 // 2. Unprotected header
57 encode_map_direct(&self.header.unprotected, &mut enc)?;
58
59 // 3. Payload (encoded as CBOR and then as bstr)
60 let claims_map = self.claims.to_map();
61 let claims_bytes = encode_map(&claims_map)?;
62 enc.bytes(&claims_bytes)?;
63
64 // 4. Signature/MAC
65 enc.bytes(&self.signature)?;
66
67 Ok(buf)
68 }
69
70 /// Decode a token from CBOR bytes
71 ///
72 /// This function supports both COSE_Sign1 (tag 18) and COSE_Mac0 (tag 17) structures,
73 /// as well as custom tags. It will automatically skip any tags and process the underlying
74 /// CBOR array.
75 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
76 let mut dec = Decoder::new(bytes);
77
78 // Check if the token starts with a tag (COSE_Sign1 tag = 18, COSE_Mac0 tag = 17, or custom tag = 61)
79 if dec.datatype()? == minicbor::data::Type::Tag {
80 // Skip the tag
81 let _ = dec.tag()?;
82
83 // Check for a second tag
84 if dec.datatype()? == minicbor::data::Type::Tag {
85 let _ = dec.tag()?;
86 }
87 }
88
89 // Expect array with 4 items
90 let array_len = dec.array()?.unwrap_or(0);
91 if array_len != 4 {
92 return Err(Error::InvalidFormat(format!(
93 "Expected array of length 4, got {array_len}"
94 )));
95 }
96
97 // 1. Protected header
98 let protected_bytes = dec.bytes()?;
99 let protected = decode_map(protected_bytes)?;
100
101 // 2. Unprotected header
102 let unprotected = decode_map_direct(&mut dec)?;
103
104 // Create header
105 let header = Header {
106 protected,
107 unprotected,
108 };
109
110 // 3. Payload
111 let claims_bytes = dec.bytes()?;
112 let claims_map = decode_map(claims_bytes)?;
113 let claims = Claims::from_map(&claims_map);
114
115 // 4. Signature
116 let signature = dec.bytes()?.to_vec();
117
118 Ok(Self {
119 header,
120 claims,
121 signature,
122 original_payload_bytes: Some(claims_bytes.to_vec()),
123 })
124 }
125
126 /// Verify the token signature
127 ///
128 /// This function supports both COSE_Sign1 and COSE_Mac0 structures.
129 /// It will first try to verify the signature using the COSE_Sign1 structure,
130 /// and if that fails, it will try the COSE_Mac0 structure.
131 pub fn verify(&self, key: &[u8]) -> Result<(), Error> {
132 let alg = self.header.algorithm().ok_or_else(|| {
133 Error::InvalidFormat("Missing algorithm in protected header".to_string())
134 })?;
135
136 match alg {
137 Algorithm::HmacSha256 => {
138 // Try with COSE_Sign1 structure first
139 let sign1_input = self.sign1_input()?;
140 let sign1_result = verify_hmac_sha256(key, &sign1_input, &self.signature);
141
142 if sign1_result.is_ok() {
143 return Ok(());
144 }
145
146 // If COSE_Sign1 verification fails, try COSE_Mac0 structure
147 let mac0_input = self.mac0_input()?;
148 verify_hmac_sha256(key, &mac0_input, &self.signature)
149 }
150 }
151 }
152
153 /// Verify the token claims
154 pub fn verify_claims(&self, options: &VerificationOptions) -> Result<(), Error> {
155 let now = current_timestamp();
156
157 // Check expiration
158 if options.verify_exp {
159 if let Some(exp) = self.claims.registered.exp {
160 if now >= exp {
161 return Err(Error::Expired);
162 }
163 } else if options.require_exp {
164 return Err(Error::MissingClaim("exp".to_string()));
165 }
166 }
167
168 // Check not before
169 if options.verify_nbf {
170 if let Some(nbf) = self.claims.registered.nbf {
171 if now < nbf {
172 return Err(Error::NotYetValid);
173 }
174 }
175 }
176
177 // Check issuer
178 if let Some(expected_iss) = &options.expected_issuer {
179 if let Some(iss) = &self.claims.registered.iss {
180 if iss != expected_iss {
181 return Err(Error::InvalidIssuer);
182 }
183 } else if options.require_iss {
184 return Err(Error::MissingClaim("iss".to_string()));
185 }
186 }
187
188 // Check audience
189 if let Some(expected_aud) = &options.expected_audience {
190 if let Some(aud) = &self.claims.registered.aud {
191 if aud != expected_aud {
192 return Err(Error::InvalidAudience);
193 }
194 } else if options.require_aud {
195 return Err(Error::MissingClaim("aud".to_string()));
196 }
197 }
198
199 // Check CAT-specific claims
200 if options.verify_catu {
201 self.verify_catu_claim(options)?;
202 }
203
204 if options.verify_catm {
205 self.verify_catm_claim(options)?;
206 }
207
208 if options.verify_catreplay {
209 self.verify_catreplay_claim(options)?;
210 }
211
212 Ok(())
213 }
214
215 /// Verify the CATU (URI) claim against the provided URI
216 fn verify_catu_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
217 use crate::constants::{cat_keys, uri_components};
218 use url::Url;
219
220 // Get the URI to verify against
221 let uri = match &options.uri {
222 Some(uri) => uri,
223 None => {
224 return Err(Error::InvalidClaimValue(
225 "No URI provided for CATU verification".to_string(),
226 ))
227 }
228 };
229
230 // Parse the URI
231 let parsed_uri = match Url::parse(uri) {
232 Ok(url) => url,
233 Err(_) => {
234 return Err(Error::InvalidClaimValue(format!(
235 "Invalid URI format: {uri}"
236 )))
237 }
238 };
239
240 // Parse the Path from the URI
241 let parsed_path = Path::new(parsed_uri.path());
242
243 // Check if token has CATU claim
244 let catu_claim = match self.claims.custom.get(&cat_keys::CATU) {
245 Some(claim) => claim,
246 None => return Ok(()), // No CATU claim, so nothing to verify
247 };
248
249 // CATU claim should be a map
250 let component_map = match catu_claim {
251 CborValue::Map(map) => map,
252 _ => {
253 return Err(Error::InvalidUriClaim(
254 "CATU claim is not a map".to_string(),
255 ))
256 }
257 };
258
259 // Verify each component in the CATU claim
260 for (component_key, component_value) in component_map {
261 match *component_key {
262 uri_components::SCHEME => {
263 self.verify_uri_component(
264 &parsed_uri.scheme().to_string(),
265 component_value,
266 "scheme",
267 )?;
268 }
269 uri_components::HOST => {
270 self.verify_uri_component(
271 &parsed_uri.host_str().unwrap_or("").to_string(),
272 component_value,
273 "host",
274 )?;
275 }
276 uri_components::PORT => {
277 let port = parsed_uri.port().map(|p| p.to_string()).unwrap_or_default();
278 self.verify_uri_component(&port, component_value, "port")?;
279 }
280 uri_components::PATH => {
281 self.verify_uri_component(
282 &parsed_uri.path().to_string(),
283 component_value,
284 "path",
285 )?;
286 }
287 uri_components::QUERY => {
288 let query = parsed_uri.query().unwrap_or("").to_string();
289 self.verify_uri_component(&query, component_value, "query")?;
290 }
291 uri_components::PARENT_PATH => {
292 // Extract parent directory path from URI path.
293 // For URI "https://example.com/a/b/file.txt", this extracts "/a/b".
294 // For root-level files, this returns an empty string.
295 // Non-UTF8 paths are converted to empty strings.
296 let parent_path = parsed_path.parent().unwrap_or(Path::new("")).to_str().unwrap_or("").to_string();
297 self.verify_uri_component(&parent_path, component_value, "parent_path")?;
298 }
299 uri_components::FILENAME => {
300 // Extract complete filename (with extension) from URI path.
301 // For URI "https://example.com/path/video.mp4", this extracts "video.mp4".
302 // For paths without a filename, this returns an empty string.
303 // Non-UTF8 filenames are converted to empty strings.
304 let filename = parsed_path.file_name().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_string();
305 self.verify_uri_component(&filename, component_value, "filename")?;
306 }
307 uri_components::STEM => {
308 // Extract filename without extension from URI path.
309 // For URI "https://example.com/path/video.mp4", this extracts "video".
310 // For "archive.tar.gz", this extracts "archive.tar" (only last extension removed).
311 // For files without extension, returns the entire filename.
312 // Non-UTF8 stems are converted to empty strings.
313 let stem = parsed_path.file_stem().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_string();
314 self.verify_uri_component(&stem, component_value, "stem")?;
315 }
316 uri_components::EXTENSION => {
317 // Extract file extension from path
318 let path = parsed_uri.path();
319 let extension = path.split('.').next_back().unwrap_or("").to_string();
320 if !path.contains('.') || path.ends_with('.') {
321 // No extension or ends with dot
322 self.verify_uri_component(&"".to_string(), component_value, "extension")?;
323 } else {
324 self.verify_uri_component(
325 &format!(".{extension}"),
326 component_value,
327 "extension",
328 )?;
329 }
330 }
331 _ => {
332 // Ignore unsupported components
333 }
334 }
335 }
336
337 Ok(())
338 }
339
340 /// Verify a URI component against match conditions
341 fn verify_uri_component(
342 &self,
343 component: &String,
344 match_conditions: &CborValue,
345 component_name: &str,
346 ) -> Result<(), Error> {
347 use crate::constants::match_types;
348 use hmac_sha256::Hash as Sha256Hash;
349 use hmac_sha512::Hash as Sha512Hash;
350 use regex::Regex;
351
352 // Match conditions should be a map
353 let match_map = match match_conditions {
354 CborValue::Map(map) => map,
355 _ => {
356 return Err(Error::InvalidUriClaim(format!(
357 "Match conditions for {component_name} is not a map"
358 )))
359 }
360 };
361
362 for (match_type, match_value) in match_map {
363 match *match_type {
364 match_types::EXACT => {
365 if let CborValue::Text(text) = match_value {
366 if component != text {
367 return Err(Error::InvalidUriClaim(format!(
368 "URI component {component_name} '{component}' does not exactly match required value '{text}'"
369 )));
370 }
371 }
372 }
373 match_types::PREFIX => {
374 if let CborValue::Text(prefix) = match_value {
375 if !component.starts_with(prefix) {
376 return Err(Error::InvalidUriClaim(format!(
377 "URI component {component_name} '{component}' does not start with required prefix '{prefix}'"
378 )));
379 }
380 }
381 }
382 match_types::SUFFIX => {
383 if let CborValue::Text(suffix) = match_value {
384 if !component.ends_with(suffix) {
385 return Err(Error::InvalidUriClaim(format!(
386 "URI component {component_name} '{component}' does not end with required suffix '{suffix}'"
387 )));
388 }
389 }
390 }
391 match_types::CONTAINS => {
392 if let CborValue::Text(contained) = match_value {
393 if !component.contains(contained) {
394 return Err(Error::InvalidUriClaim(format!(
395 "URI component {component_name} '{component}' does not contain required text '{contained}'"
396 )));
397 }
398 }
399 }
400 match_types::REGEX => {
401 if let CborValue::Array(array) = match_value {
402 if let Some(CborValue::Text(pattern)) = array.first() {
403 match Regex::new(pattern) {
404 Ok(regex) => {
405 if !regex.is_match(component) {
406 return Err(Error::InvalidUriClaim(format!(
407 "URI component {component_name} '{component}' does not match required regex pattern '{pattern}'"
408 )));
409 }
410 }
411 Err(_) => {
412 return Err(Error::InvalidUriClaim(format!(
413 "Invalid regex pattern: {pattern}"
414 )))
415 }
416 }
417 }
418 }
419 }
420 match_types::SHA256 => {
421 if let CborValue::Bytes(expected_hash) = match_value {
422 let hash = Sha256Hash::hash(component.as_bytes());
423
424 if !ct_codecs::verify(&hash, expected_hash.as_slice()) {
425 return Err(Error::InvalidUriClaim(format!(
426 "URI component {component_name} '{component}' SHA-256 hash does not match expected value"
427 )));
428 }
429 }
430 }
431 match_types::SHA512_256 => {
432 if let CborValue::Bytes(expected_hash) = match_value {
433 let hash = Sha512Hash::hash(component.as_bytes());
434 let truncated_hash = &hash[0..32]; // Take first 256 bits (32 bytes)
435
436 if !ct_codecs::verify(truncated_hash, &expected_hash[..]) {
437 return Err(Error::InvalidUriClaim(format!(
438 "URI component {component_name} '{component}' SHA-512/256 hash does not match expected value"
439 )));
440 }
441 }
442 }
443 _ => {
444 // Ignore unsupported match types
445 }
446 }
447 }
448
449 Ok(())
450 }
451
452 /// Verify the CATM (HTTP method) claim against the provided method
453 fn verify_catm_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
454 use crate::constants::cat_keys;
455
456 // Get the HTTP method to verify against
457 let method = match &options.http_method {
458 Some(method) => method,
459 None => {
460 return Err(Error::InvalidClaimValue(
461 "No HTTP method provided for CATM verification".to_string(),
462 ))
463 }
464 };
465
466 // Check if token has CATM claim
467 let catm_claim = match self.claims.custom.get(&cat_keys::CATM) {
468 Some(claim) => claim,
469 None => return Ok(()), // No CATM claim, so nothing to verify
470 };
471
472 // CATM claim should be an array of allowed methods
473 let allowed_methods = match catm_claim {
474 CborValue::Array(methods) => methods,
475 _ => {
476 return Err(Error::InvalidMethodClaim(
477 "CATM claim is not an array".to_string(),
478 ))
479 }
480 };
481
482 // Check if the provided method is in the allowed methods list
483 let method_upper = method.to_uppercase();
484 let method_allowed = allowed_methods.iter().any(|m| {
485 if let CborValue::Text(allowed) = m {
486 allowed.to_uppercase() == method_upper
487 } else {
488 false
489 }
490 });
491
492 if !method_allowed {
493 return Err(Error::InvalidMethodClaim(format!(
494 "HTTP method '{}' is not allowed. Permitted methods: {:?}",
495 method,
496 allowed_methods
497 .iter()
498 .filter_map(|m| if let CborValue::Text(t) = m {
499 Some(t.as_str())
500 } else {
501 None
502 })
503 .collect::<Vec<&str>>()
504 )));
505 }
506
507 Ok(())
508 }
509
510 /// Verify the CATREPLAY claim for token replay protection
511 fn verify_catreplay_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
512 use crate::constants::{cat_keys, replay_values};
513
514 // Check if token has CATREPLAY claim
515 let catreplay_claim = match self.claims.custom.get(&cat_keys::CATREPLAY) {
516 Some(claim) => claim,
517 None => return Ok(()), // No CATREPLAY claim, so nothing to verify
518 };
519
520 // Get the replay protection value
521 let replay_value = match catreplay_claim {
522 CborValue::Integer(value) => *value as i32,
523 _ => {
524 return Err(Error::InvalidClaimValue(
525 "CATREPLAY claim is not an integer".to_string(),
526 ))
527 }
528 };
529
530 match replay_value {
531 replay_values::PERMITTED => {
532 // Replay is permitted, no verification needed
533 Ok(())
534 }
535 replay_values::PROHIBITED => {
536 // Replay is prohibited, check if token has been seen before
537 if options.token_seen_before {
538 Err(Error::ReplayViolation(
539 "Token replay is prohibited".to_string(),
540 ))
541 } else {
542 Ok(())
543 }
544 }
545 replay_values::REUSE_DETECTION => {
546 // Reuse is detected but allowed, no error returned
547 // Implementations should log or notify about reuse
548 Ok(())
549 }
550 _ => Err(Error::InvalidClaimValue(format!(
551 "Invalid CATREPLAY value: {replay_value}"
552 ))),
553 }
554 }
555
556 // Note: signature_input method removed as we now use mac0_input for HMAC algorithms
557
558 /// Get the encoded payload bytes, using original bytes if available
559 fn get_payload_bytes(&self) -> Result<Vec<u8>, Error> {
560 if let Some(ref original) = self.original_payload_bytes {
561 // Use original bytes for verification
562 Ok(original.clone())
563 } else {
564 // Encode claims for newly created tokens
565 let claims_map = self.claims.to_map();
566 encode_map(&claims_map)
567 }
568 }
569
570 /// Get the COSE_Sign1 signature input
571 fn sign1_input(&self) -> Result<Vec<u8>, Error> {
572 // Sig_structure = [
573 // context : "Signature1",
574 // protected : bstr .cbor header_map,
575 // external_aad : bstr,
576 // payload : bstr .cbor claims
577 // ]
578
579 let mut buf = Vec::new();
580 let mut enc = Encoder::new(&mut buf);
581
582 // Start array with 4 items
583 enc.array(4)?;
584
585 // 1. Context
586 enc.str("Signature1")?;
587
588 // 2. Protected header
589 let protected_bytes = encode_map(&self.header.protected)?;
590 enc.bytes(&protected_bytes)?;
591
592 // 3. External AAD (empty in our case)
593 enc.bytes(&[])?;
594
595 // 4. Payload
596 let claims_bytes = self.get_payload_bytes()?;
597 enc.bytes(&claims_bytes)?;
598
599 Ok(buf)
600 }
601
602 /// Get the COSE_Mac0 signature input
603 fn mac0_input(&self) -> Result<Vec<u8>, Error> {
604 // Mac_structure = [
605 // context : "MAC0",
606 // protected : bstr .cbor header_map,
607 // external_aad : bstr,
608 // payload : bstr .cbor claims
609 // ]
610
611 let mut buf = Vec::new();
612 let mut enc = Encoder::new(&mut buf);
613
614 // Start array with 4 items
615 enc.array(4)?;
616
617 // 1. Context
618 enc.str("MAC0")?;
619
620 // 2. Protected header
621 let protected_bytes = encode_map(&self.header.protected)?;
622 enc.bytes(&protected_bytes)?;
623
624 // 3. External AAD (empty in our case)
625 enc.bytes(&[])?;
626
627 // 4. Payload
628 let claims_bytes = self.get_payload_bytes()?;
629 enc.bytes(&claims_bytes)?;
630
631 Ok(buf)
632 }
633
634 // Convenience methods for common token operations
635
636 /// Check if the token has expired
637 ///
638 /// Returns `true` if the token has an expiration claim and the current time is at or after it.
639 /// Returns `false` if the token has no expiration claim or if it hasn't expired yet.
640 ///
641 /// # Example
642 ///
643 /// ```
644 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
645 ///
646 /// let key = b"my-secret-key";
647 /// let now = current_timestamp();
648 ///
649 /// // Token that expires in 1 hour
650 /// let token = TokenBuilder::new()
651 /// .algorithm(Algorithm::HmacSha256)
652 /// .registered_claims(RegisteredClaims::new().with_expiration(now + 3600))
653 /// .sign(key)
654 /// .unwrap();
655 ///
656 /// assert!(!token.is_expired());
657 /// ```
658 pub fn is_expired(&self) -> bool {
659 if let Some(exp) = self.claims.registered.exp {
660 current_timestamp() >= exp
661 } else {
662 false
663 }
664 }
665
666 /// Get the duration until token expiration
667 ///
668 /// Returns `Some(Duration)` if the token has an expiration claim and hasn't expired yet.
669 /// Returns `None` if the token has no expiration claim or has already expired.
670 ///
671 /// # Example
672 ///
673 /// ```
674 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
675 ///
676 /// let key = b"my-secret-key";
677 /// let now = current_timestamp();
678 ///
679 /// let token = TokenBuilder::new()
680 /// .algorithm(Algorithm::HmacSha256)
681 /// .registered_claims(RegisteredClaims::new().with_expiration(now + 3600))
682 /// .sign(key)
683 /// .unwrap();
684 ///
685 /// if let Some(duration) = token.expires_in() {
686 /// println!("Token expires in {} seconds", duration.as_secs());
687 /// }
688 /// ```
689 pub fn expires_in(&self) -> Option<std::time::Duration> {
690 if let Some(exp) = self.claims.registered.exp {
691 let now = current_timestamp();
692 if now < exp {
693 Some(std::time::Duration::from_secs(exp - now))
694 } else {
695 None
696 }
697 } else {
698 None
699 }
700 }
701
702 /// Check if the token is valid based on the not-before (nbf) claim
703 ///
704 /// Returns `true` if the token has no nbf claim or if the current time is at or after it.
705 /// Returns `false` if the token has an nbf claim and the current time is before it.
706 ///
707 /// # Example
708 ///
709 /// ```
710 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
711 ///
712 /// let key = b"my-secret-key";
713 /// let now = current_timestamp();
714 ///
715 /// let token = TokenBuilder::new()
716 /// .algorithm(Algorithm::HmacSha256)
717 /// .registered_claims(RegisteredClaims::new().with_not_before(now))
718 /// .sign(key)
719 /// .unwrap();
720 ///
721 /// assert!(token.is_valid_yet());
722 /// ```
723 pub fn is_valid_yet(&self) -> bool {
724 if let Some(nbf) = self.claims.registered.nbf {
725 current_timestamp() >= nbf
726 } else {
727 true
728 }
729 }
730
731 /// Get the issuer claim value
732 ///
733 /// # Example
734 ///
735 /// ```
736 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
737 ///
738 /// let key = b"my-secret-key";
739 /// let token = TokenBuilder::new()
740 /// .algorithm(Algorithm::HmacSha256)
741 /// .registered_claims(RegisteredClaims::new().with_issuer("example-issuer"))
742 /// .sign(key)
743 /// .unwrap();
744 ///
745 /// assert_eq!(token.issuer(), Some("example-issuer"));
746 /// ```
747 pub fn issuer(&self) -> Option<&str> {
748 self.claims.registered.iss.as_deref()
749 }
750
751 /// Get the subject claim value
752 ///
753 /// # Example
754 ///
755 /// ```
756 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
757 ///
758 /// let key = b"my-secret-key";
759 /// let token = TokenBuilder::new()
760 /// .algorithm(Algorithm::HmacSha256)
761 /// .registered_claims(RegisteredClaims::new().with_subject("user-123"))
762 /// .sign(key)
763 /// .unwrap();
764 ///
765 /// assert_eq!(token.subject(), Some("user-123"));
766 /// ```
767 pub fn subject(&self) -> Option<&str> {
768 self.claims.registered.sub.as_deref()
769 }
770
771 /// Get the audience claim value
772 ///
773 /// # Example
774 ///
775 /// ```
776 /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
777 ///
778 /// let key = b"my-secret-key";
779 /// let token = TokenBuilder::new()
780 /// .algorithm(Algorithm::HmacSha256)
781 /// .registered_claims(RegisteredClaims::new().with_audience("api-service"))
782 /// .sign(key)
783 /// .unwrap();
784 ///
785 /// assert_eq!(token.audience(), Some("api-service"));
786 /// ```
787 pub fn audience(&self) -> Option<&str> {
788 self.claims.registered.aud.as_deref()
789 }
790
791 /// Get the expiration timestamp
792 pub fn expiration(&self) -> Option<u64> {
793 self.claims.registered.exp
794 }
795
796 /// Get the not-before timestamp
797 pub fn not_before(&self) -> Option<u64> {
798 self.claims.registered.nbf
799 }
800
801 /// Get the issued-at timestamp
802 pub fn issued_at(&self) -> Option<u64> {
803 self.claims.registered.iat
804 }
805
806 /// Get a custom claim as a string
807 ///
808 /// Returns `Some(&str)` if the claim exists and is a text value, `None` otherwise.
809 ///
810 /// # Example
811 ///
812 /// ```
813 /// use common_access_token::{TokenBuilder, Algorithm};
814 ///
815 /// let key = b"my-secret-key";
816 /// let token = TokenBuilder::new()
817 /// .algorithm(Algorithm::HmacSha256)
818 /// .custom_string(100, "custom-value")
819 /// .sign(key)
820 /// .unwrap();
821 ///
822 /// assert_eq!(token.get_custom_string(100), Some("custom-value"));
823 /// assert_eq!(token.get_custom_string(999), None);
824 /// ```
825 pub fn get_custom_string(&self, key: i32) -> Option<&str> {
826 match self.claims.custom.get(&key) {
827 Some(CborValue::Text(s)) => Some(s.as_str()),
828 _ => None,
829 }
830 }
831
832 /// Get a custom claim as an integer
833 ///
834 /// Returns `Some(i64)` if the claim exists and is an integer value, `None` otherwise.
835 ///
836 /// # Example
837 ///
838 /// ```
839 /// use common_access_token::{TokenBuilder, Algorithm};
840 ///
841 /// let key = b"my-secret-key";
842 /// let token = TokenBuilder::new()
843 /// .algorithm(Algorithm::HmacSha256)
844 /// .custom_int(100, 42)
845 /// .sign(key)
846 /// .unwrap();
847 ///
848 /// assert_eq!(token.get_custom_int(100), Some(42));
849 /// assert_eq!(token.get_custom_int(999), None);
850 /// ```
851 pub fn get_custom_int(&self, key: i32) -> Option<i64> {
852 match self.claims.custom.get(&key) {
853 Some(CborValue::Integer(i)) => Some(*i),
854 _ => None,
855 }
856 }
857
858 /// Get a custom claim as binary data
859 ///
860 /// Returns `Some(&[u8])` if the claim exists and is a bytes value, `None` otherwise.
861 ///
862 /// # Example
863 ///
864 /// ```
865 /// use common_access_token::{TokenBuilder, Algorithm};
866 ///
867 /// let key = b"my-secret-key";
868 /// let data = vec![1, 2, 3, 4];
869 /// let token = TokenBuilder::new()
870 /// .algorithm(Algorithm::HmacSha256)
871 /// .custom_binary(100, data.clone())
872 /// .sign(key)
873 /// .unwrap();
874 ///
875 /// assert_eq!(token.get_custom_binary(100), Some(data.as_slice()));
876 /// assert_eq!(token.get_custom_binary(999), None);
877 /// ```
878 pub fn get_custom_binary(&self, key: i32) -> Option<&[u8]> {
879 match self.claims.custom.get(&key) {
880 Some(CborValue::Bytes(b)) => Some(b.as_slice()),
881 _ => None,
882 }
883 }
884
885 /// Get a reference to a custom claim value
886 ///
887 /// Returns `Some(&CborValue)` if the claim exists, `None` otherwise.
888 ///
889 /// # Example
890 ///
891 /// ```
892 /// use common_access_token::{TokenBuilder, Algorithm, CborValue};
893 ///
894 /// let key = b"my-secret-key";
895 /// let token = TokenBuilder::new()
896 /// .algorithm(Algorithm::HmacSha256)
897 /// .custom_string(100, "value")
898 /// .sign(key)
899 /// .unwrap();
900 ///
901 /// if let Some(CborValue::Text(s)) = token.get_custom_claim(100) {
902 /// assert_eq!(s, "value");
903 /// }
904 /// ```
905 pub fn get_custom_claim(&self, key: i32) -> Option<&CborValue> {
906 self.claims.custom.get(&key)
907 }
908
909 /// Check if a custom claim exists
910 ///
911 /// # Example
912 ///
913 /// ```
914 /// use common_access_token::{TokenBuilder, Algorithm};
915 ///
916 /// let key = b"my-secret-key";
917 /// let token = TokenBuilder::new()
918 /// .algorithm(Algorithm::HmacSha256)
919 /// .custom_string(100, "value")
920 /// .sign(key)
921 /// .unwrap();
922 ///
923 /// assert!(token.has_custom_claim(100));
924 /// assert!(!token.has_custom_claim(999));
925 /// ```
926 pub fn has_custom_claim(&self, key: i32) -> bool {
927 self.claims.custom.contains_key(&key)
928 }
929}
930
931/// Options for token verification
932#[derive(Debug, Clone, Default)]
933pub struct VerificationOptions {
934 /// Verify expiration claim
935 pub verify_exp: bool,
936 /// Require expiration claim
937 pub require_exp: bool,
938 /// Verify not before claim
939 pub verify_nbf: bool,
940 /// Expected issuer
941 pub expected_issuer: Option<String>,
942 /// Require issuer claim
943 pub require_iss: bool,
944 /// Expected audience
945 pub expected_audience: Option<String>,
946 /// Require audience claim
947 pub require_aud: bool,
948 /// Verify CAT-specific URI claim (CATU) against provided URI
949 pub verify_catu: bool,
950 /// URI to verify against CATU claim
951 pub uri: Option<String>,
952 /// Verify CAT-specific HTTP methods claim (CATM) against provided method
953 pub verify_catm: bool,
954 /// HTTP method to verify against CATM claim
955 pub http_method: Option<String>,
956 /// Verify CAT-specific replay protection (CATREPLAY)
957 pub verify_catreplay: bool,
958 /// Whether the token has been seen before (for replay protection)
959 pub token_seen_before: bool,
960}
961
962impl VerificationOptions {
963 /// Create new default verification options
964 pub fn new() -> Self {
965 Self {
966 verify_exp: true,
967 require_exp: false,
968 verify_nbf: true,
969 expected_issuer: None,
970 require_iss: false,
971 expected_audience: None,
972 require_aud: false,
973 verify_catu: false,
974 uri: None,
975 verify_catm: false,
976 http_method: None,
977 verify_catreplay: false,
978 token_seen_before: false,
979 }
980 }
981
982 /// Set whether to verify expiration
983 pub fn verify_exp(mut self, verify: bool) -> Self {
984 self.verify_exp = verify;
985 self
986 }
987
988 /// Set whether to require expiration
989 pub fn require_exp(mut self, require: bool) -> Self {
990 self.require_exp = require;
991 self
992 }
993
994 /// Set whether to verify not before
995 pub fn verify_nbf(mut self, verify: bool) -> Self {
996 self.verify_nbf = verify;
997 self
998 }
999
1000 /// Set expected issuer
1001 pub fn expected_issuer<S: Into<String>>(mut self, issuer: S) -> Self {
1002 self.expected_issuer = Some(issuer.into());
1003 self
1004 }
1005
1006 /// Set whether to require issuer
1007 pub fn require_iss(mut self, require: bool) -> Self {
1008 self.require_iss = require;
1009 self
1010 }
1011
1012 /// Set expected audience
1013 pub fn expected_audience<S: Into<String>>(mut self, audience: S) -> Self {
1014 self.expected_audience = Some(audience.into());
1015 self
1016 }
1017
1018 /// Set whether to require audience
1019 pub fn require_aud(mut self, require: bool) -> Self {
1020 self.require_aud = require;
1021 self
1022 }
1023
1024 /// Set whether to verify CAT-specific URI claim (CATU)
1025 pub fn verify_catu(mut self, verify: bool) -> Self {
1026 self.verify_catu = verify;
1027 self
1028 }
1029
1030 /// Set URI to verify against CATU claim
1031 pub fn uri<S: Into<String>>(mut self, uri: S) -> Self {
1032 self.uri = Some(uri.into());
1033 self
1034 }
1035
1036 /// Set whether to verify CAT-specific HTTP methods claim (CATM)
1037 pub fn verify_catm(mut self, verify: bool) -> Self {
1038 self.verify_catm = verify;
1039 self
1040 }
1041
1042 /// Set HTTP method to verify against CATM claim
1043 pub fn http_method<S: Into<String>>(mut self, method: S) -> Self {
1044 self.http_method = Some(method.into());
1045 self
1046 }
1047
1048 /// Set whether to verify CAT-specific replay protection (CATREPLAY)
1049 pub fn verify_catreplay(mut self, verify: bool) -> Self {
1050 self.verify_catreplay = verify;
1051 self
1052 }
1053
1054 /// Set whether the token has been seen before (for replay protection)
1055 pub fn token_seen_before(mut self, seen: bool) -> Self {
1056 self.token_seen_before = seen;
1057 self
1058 }
1059}
1060
1061/// Builder for creating tokens
1062#[derive(Debug, Clone, Default)]
1063pub struct TokenBuilder {
1064 header: Header,
1065 claims: Claims,
1066}
1067
1068impl TokenBuilder {
1069 /// Create a new token builder
1070 pub fn new() -> Self {
1071 Self::default()
1072 }
1073
1074 /// Set the algorithm
1075 pub fn algorithm(mut self, alg: Algorithm) -> Self {
1076 self.header = self.header.with_algorithm(alg);
1077 self
1078 }
1079
1080 /// Set the key identifier in the protected header
1081 pub fn protected_key_id(mut self, kid: KeyId) -> Self {
1082 self.header = self.header.with_protected_key_id(kid);
1083 self
1084 }
1085
1086 /// Set the key identifier in the unprotected header
1087 pub fn unprotected_key_id(mut self, kid: KeyId) -> Self {
1088 self.header = self.header.with_unprotected_key_id(kid);
1089 self
1090 }
1091
1092 /// Set the registered claims
1093 pub fn registered_claims(mut self, claims: RegisteredClaims) -> Self {
1094 self.claims = self.claims.with_registered_claims(claims);
1095 self
1096 }
1097
1098 /// Add a custom claim with a string value
1099 pub fn custom_string<S: Into<String>>(mut self, key: i32, value: S) -> Self {
1100 self.claims = self.claims.with_custom_string(key, value);
1101 self
1102 }
1103
1104 /// Add a custom claim with a binary value
1105 pub fn custom_binary<B: Into<Vec<u8>>>(mut self, key: i32, value: B) -> Self {
1106 self.claims = self.claims.with_custom_binary(key, value);
1107 self
1108 }
1109
1110 /// Add a custom claim with an integer value
1111 pub fn custom_int(mut self, key: i32, value: i64) -> Self {
1112 self.claims = self.claims.with_custom_int(key, value);
1113 self
1114 }
1115
1116 /// Add a custom claim with a nested map value
1117 pub fn custom_map(mut self, key: i32, value: BTreeMap<i32, CborValue>) -> Self {
1118 self.claims = self.claims.with_custom_map(key, value);
1119 self
1120 }
1121
1122 /// Add a custom claim with a CborValue directly
1123 pub fn custom_cbor(mut self, key: i32, value: CborValue) -> Self {
1124 self.claims.custom.insert(key, value);
1125 self
1126 }
1127
1128 /// Add a custom claim with an array value
1129 pub fn custom_array(mut self, key: i32, value: Vec<CborValue>) -> Self {
1130 self.claims.custom.insert(key, CborValue::Array(value));
1131 self
1132 }
1133
1134 /// Set expiration time relative to now (in seconds)
1135 ///
1136 /// This is a convenience method that sets the expiration claim to the current time plus the specified number of seconds.
1137 ///
1138 /// # Example
1139 ///
1140 /// ```
1141 /// use common_access_token::{TokenBuilder, Algorithm, current_timestamp};
1142 ///
1143 /// let key = b"my-secret-key";
1144 ///
1145 /// // Token expires in 1 hour
1146 /// let token = TokenBuilder::new()
1147 /// .algorithm(Algorithm::HmacSha256)
1148 /// .expires_in_secs(3600)
1149 /// .sign(key)
1150 /// .unwrap();
1151 ///
1152 /// assert!(!token.is_expired());
1153 /// ```
1154 pub fn expires_in_secs(mut self, seconds: u64) -> Self {
1155 let exp = current_timestamp() + seconds;
1156 self.claims.registered.exp = Some(exp);
1157 self
1158 }
1159
1160 /// Set expiration time relative to now using a Duration
1161 ///
1162 /// This is a convenience method that sets the expiration claim to the current time plus the specified duration.
1163 ///
1164 /// # Example
1165 ///
1166 /// ```
1167 /// use common_access_token::{TokenBuilder, Algorithm};
1168 /// use std::time::Duration;
1169 ///
1170 /// let key = b"my-secret-key";
1171 ///
1172 /// // Token expires in 1 hour
1173 /// let token = TokenBuilder::new()
1174 /// .algorithm(Algorithm::HmacSha256)
1175 /// .expires_in(Duration::from_secs(3600))
1176 /// .sign(key)
1177 /// .unwrap();
1178 ///
1179 /// assert!(!token.is_expired());
1180 /// ```
1181 pub fn expires_in(self, duration: std::time::Duration) -> Self {
1182 self.expires_in_secs(duration.as_secs())
1183 }
1184
1185 /// Set token lifetime with issued-at and expiration claims
1186 ///
1187 /// This convenience method sets both the `iat` (issued at) and `exp` (expiration) claims.
1188 /// The issued-at is set to the current time, and expiration is set to current time plus the specified seconds.
1189 ///
1190 /// # Example
1191 ///
1192 /// ```
1193 /// use common_access_token::{TokenBuilder, Algorithm};
1194 ///
1195 /// let key = b"my-secret-key";
1196 ///
1197 /// // Token valid for 1 hour
1198 /// let token = TokenBuilder::new()
1199 /// .algorithm(Algorithm::HmacSha256)
1200 /// .valid_for_secs(3600)
1201 /// .sign(key)
1202 /// .unwrap();
1203 ///
1204 /// assert!(token.issued_at().is_some());
1205 /// assert!(token.expiration().is_some());
1206 /// ```
1207 pub fn valid_for_secs(mut self, seconds: u64) -> Self {
1208 let now = current_timestamp();
1209 self.claims.registered.iat = Some(now);
1210 self.claims.registered.exp = Some(now + seconds);
1211 self
1212 }
1213
1214 /// Set token lifetime with issued-at and expiration claims using a Duration
1215 ///
1216 /// This convenience method sets both the `iat` (issued at) and `exp` (expiration) claims.
1217 /// The issued-at is set to the current time, and expiration is set to current time plus the specified duration.
1218 ///
1219 /// # Example
1220 ///
1221 /// ```
1222 /// use common_access_token::{TokenBuilder, Algorithm};
1223 /// use std::time::Duration;
1224 ///
1225 /// let key = b"my-secret-key";
1226 ///
1227 /// // Token valid for 1 hour
1228 /// let token = TokenBuilder::new()
1229 /// .algorithm(Algorithm::HmacSha256)
1230 /// .valid_for(Duration::from_secs(3600))
1231 /// .sign(key)
1232 /// .unwrap();
1233 ///
1234 /// assert!(token.issued_at().is_some());
1235 /// assert!(token.expiration().is_some());
1236 /// ```
1237 pub fn valid_for(self, duration: std::time::Duration) -> Self {
1238 self.valid_for_secs(duration.as_secs())
1239 }
1240
1241 /// Build and sign the token
1242 pub fn sign(self, key: &[u8]) -> Result<Token, Error> {
1243 // Ensure we have an algorithm
1244 let alg = self.header.algorithm().ok_or_else(|| {
1245 Error::InvalidFormat("Missing algorithm in protected header".to_string())
1246 })?;
1247
1248 // Create token without signature
1249 let token = Token {
1250 header: self.header,
1251 claims: self.claims,
1252 signature: Vec::new(),
1253 original_payload_bytes: None,
1254 };
1255
1256 // Compute signature input based on algorithm
1257 // HMAC algorithms use COSE_Mac0 structure, others use COSE_Sign1
1258 let (_signature_input, signature) = match alg {
1259 Algorithm::HmacSha256 => {
1260 let mac_input = token.mac0_input()?;
1261 let mac = compute_hmac_sha256(key, &mac_input);
1262 (mac_input, mac)
1263 }
1264 };
1265
1266 // Create final token with signature
1267 Ok(Token {
1268 header: token.header,
1269 claims: token.claims,
1270 signature,
1271 original_payload_bytes: None,
1272 })
1273 }
1274}
1275
1276// Helper functions for CBOR encoding/decoding
1277
1278fn encode_map(map: &HeaderMap) -> Result<Vec<u8>, Error> {
1279 let mut buf = Vec::new();
1280 let mut enc = Encoder::new(&mut buf);
1281
1282 encode_map_direct(map, &mut enc)?;
1283
1284 Ok(buf)
1285}
1286
1287/// Encode a CBOR value directly to the encoder
1288fn encode_cbor_value(value: &CborValue, enc: &mut Encoder<&mut Vec<u8>>) -> Result<(), Error> {
1289 match value {
1290 CborValue::Integer(i) => {
1291 enc.i64(*i)?;
1292 }
1293 CborValue::Bytes(b) => {
1294 enc.bytes(b)?;
1295 }
1296 CborValue::Text(s) => {
1297 enc.str(s)?;
1298 }
1299 CborValue::Map(nested_map) => {
1300 // Create a nested encoder for the map
1301 encode_map_direct(nested_map, enc)?;
1302 }
1303 CborValue::Array(arr) => {
1304 // Create a nested encoder for the array
1305 enc.array(arr.len() as u64)?;
1306 for item in arr {
1307 encode_cbor_value(item, enc)?;
1308 }
1309 }
1310 CborValue::Null => {
1311 enc.null()?;
1312 }
1313 }
1314 Ok(())
1315}
1316
1317fn encode_map_direct(map: &HeaderMap, enc: &mut Encoder<&mut Vec<u8>>) -> Result<(), Error> {
1318 enc.map(map.len() as u64)?;
1319
1320 for (key, value) in map {
1321 enc.i32(*key)?;
1322 encode_cbor_value(value, enc)?;
1323 }
1324
1325 Ok(())
1326}
1327
1328fn decode_map(bytes: &[u8]) -> Result<HeaderMap, Error> {
1329 let mut dec = Decoder::new(bytes);
1330 decode_map_direct(&mut dec)
1331}
1332
1333/// Decode a CBOR array
1334fn decode_array(dec: &mut Decoder<'_>) -> Result<Vec<CborValue>, Error> {
1335 let array_len = dec.array()?.unwrap_or(0);
1336 let mut array = Vec::with_capacity(array_len as usize);
1337
1338 for _ in 0..array_len {
1339 // Try to decode based on the datatype
1340 let datatype = dec.datatype()?;
1341
1342 // Handle each type separately
1343 let value = if datatype == minicbor::data::Type::Int {
1344 // Integer value
1345 let i = dec.i64()?;
1346 CborValue::Integer(i)
1347 } else if datatype == minicbor::data::Type::U8
1348 || datatype == minicbor::data::Type::U16
1349 || datatype == minicbor::data::Type::U32
1350 || datatype == minicbor::data::Type::U64
1351 {
1352 // Unsigned integer value
1353 let i = dec.u64()? as i64;
1354 CborValue::Integer(i)
1355 } else if datatype == minicbor::data::Type::Bytes {
1356 // Byte string
1357 let b = dec.bytes()?;
1358 CborValue::Bytes(b.to_vec())
1359 } else if datatype == minicbor::data::Type::String {
1360 // Text string
1361 let s = dec.str()?;
1362 CborValue::Text(s.to_string())
1363 } else if datatype == minicbor::data::Type::Map {
1364 // Nested map
1365 let nested_map = decode_map_direct(dec)?;
1366 CborValue::Map(nested_map)
1367 } else if datatype == minicbor::data::Type::Array {
1368 // Nested array
1369 let nested_array = decode_array(dec)?;
1370 CborValue::Array(nested_array)
1371 } else if datatype == minicbor::data::Type::Null {
1372 // Null value
1373 dec.null()?;
1374 CborValue::Null
1375 } else {
1376 // Unsupported type
1377 return Err(Error::InvalidFormat(format!(
1378 "Unsupported CBOR type in array: {datatype:?}"
1379 )));
1380 };
1381
1382 array.push(value);
1383 }
1384
1385 Ok(array)
1386}
1387
1388fn decode_map_direct(dec: &mut Decoder<'_>) -> Result<HeaderMap, Error> {
1389 let map_len = dec.map()?.unwrap_or(0);
1390 let mut map = HeaderMap::new();
1391
1392 for _ in 0..map_len {
1393 let key = dec.i32()?;
1394
1395 // Try to decode based on the datatype
1396 let datatype = dec.datatype()?;
1397
1398 // Handle each type separately
1399 let value = if datatype == minicbor::data::Type::Int {
1400 // Integer value
1401 let i = dec.i64()?;
1402 CborValue::Integer(i)
1403 } else if datatype == minicbor::data::Type::U8
1404 || datatype == minicbor::data::Type::U16
1405 || datatype == minicbor::data::Type::U32
1406 || datatype == minicbor::data::Type::U64
1407 {
1408 // Unsigned integer value
1409 let i = dec.u64()? as i64;
1410 CborValue::Integer(i)
1411 } else if datatype == minicbor::data::Type::Bytes {
1412 // Byte string
1413 let b = dec.bytes()?;
1414 CborValue::Bytes(b.to_vec())
1415 } else if datatype == minicbor::data::Type::String {
1416 // Text string
1417 let s = dec.str()?;
1418 CborValue::Text(s.to_string())
1419 } else if datatype == minicbor::data::Type::Map {
1420 // Nested map
1421 let nested_map = decode_map_direct(dec)?;
1422 CborValue::Map(nested_map)
1423 } else if datatype == minicbor::data::Type::Array {
1424 // Array
1425 let array = decode_array(dec)?;
1426 CborValue::Array(array)
1427 } else if datatype == minicbor::data::Type::Null {
1428 // Null value
1429 dec.null()?;
1430 CborValue::Null
1431 } else {
1432 // Unsupported type
1433 return Err(Error::InvalidFormat(format!(
1434 "Unsupported CBOR type: {datatype:?}"
1435 )));
1436 };
1437
1438 map.insert(key, value);
1439 }
1440
1441 Ok(map)
1442}