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