1use crate::errors::{AuthError, Result};
18use base64::Engine as _;
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::collections::HashMap;
22use std::sync::Arc;
23use tokio::sync::RwLock;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct GnapConfig {
27 pub enabled: bool,
28 pub transaction_endpoint: String,
29 pub interaction_base_url: Option<String>,
31 pub token_lifetime_secs: u64,
33 pub transaction_lifetime_secs: u64,
35}
36
37impl Default for GnapConfig {
38 fn default() -> Self {
39 Self {
40 enabled: false,
41 transaction_endpoint: "/api/gnap/tx".to_string(),
42 interaction_base_url: None,
43 token_lifetime_secs: 3600,
44 transaction_lifetime_secs: 600,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct GnapTransactionRequest {
52 pub client: Option<GnapClientInfo>,
53 pub interact: Option<GnapInteractionRequirements>,
54 pub access_token: Option<Vec<GnapAccessRequest>>,
56 pub subject: Option<GnapSubjectRequest>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct GnapAccessRequest {
63 #[serde(rename = "type")]
65 pub access_type: String,
66 #[serde(default)]
68 pub actions: Vec<String>,
69 #[serde(default)]
71 pub locations: Vec<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct GnapSubjectRequest {
77 #[serde(default)]
79 pub sub_id_formats: Vec<String>,
80 #[serde(default)]
82 pub assertion_formats: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct GnapClientInfo {
87 pub key: Option<GnapClientKey>,
89 pub display: Option<GnapClientDisplay>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct GnapClientKey {
96 pub proof: String,
98 pub jwk: GnapJwk,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct GnapJwk {
105 pub kty: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub kid: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
114 pub crv: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub x: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub y: Option<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
125 pub n: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub e: Option<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct GnapClientDisplay {
133 pub name: Option<String>,
134 pub uri: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct GnapInteractionRequirements {
139 pub start: Vec<String>,
140 pub finish: Option<GnapInteractionFinish>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct GnapInteractionFinish {
145 pub method: String,
146 pub uri: String,
147 pub nonce: String,
148}
149
150impl GnapTransactionRequest {
151 pub fn builder() -> GnapTransactionRequestBuilder {
163 GnapTransactionRequestBuilder {
164 client: None,
165 interact: None,
166 access_token: Vec::new(),
167 subject: None,
168 }
169 }
170}
171
172pub struct GnapTransactionRequestBuilder {
174 client: Option<GnapClientInfo>,
175 interact: Option<GnapInteractionRequirements>,
176 access_token: Vec<GnapAccessRequest>,
177 subject: Option<GnapSubjectRequest>,
178}
179
180impl GnapTransactionRequestBuilder {
181 pub fn client(mut self, client: GnapClientInfo) -> Self {
183 self.client = Some(client);
184 self
185 }
186
187 pub fn client_key(mut self, jwk: GnapJwk, proof: impl Into<String>) -> Self {
189 self.client = Some(GnapClientInfo {
190 key: Some(GnapClientKey {
191 proof: proof.into(),
192 jwk,
193 }),
194 display: None,
195 });
196 self
197 }
198
199 pub fn redirect_interaction(
201 mut self,
202 callback_uri: impl Into<String>,
203 nonce: impl Into<String>,
204 ) -> Self {
205 self.interact = Some(GnapInteractionRequirements {
206 start: vec!["redirect".to_string()],
207 finish: Some(GnapInteractionFinish {
208 method: "redirect".to_string(),
209 uri: callback_uri.into(),
210 nonce: nonce.into(),
211 }),
212 });
213 self
214 }
215
216 pub fn interact(mut self, interact: GnapInteractionRequirements) -> Self {
218 self.interact = Some(interact);
219 self
220 }
221
222 pub fn access(
224 mut self,
225 access_type: impl Into<String>,
226 actions: &[impl AsRef<str>],
227 locations: &[impl AsRef<str>],
228 ) -> Self {
229 self.access_token.push(GnapAccessRequest {
230 access_type: access_type.into(),
231 actions: actions.iter().map(|a| a.as_ref().to_string()).collect(),
232 locations: locations.iter().map(|l| l.as_ref().to_string()).collect(),
233 });
234 self
235 }
236
237 pub fn access_type(self, access_type: impl Into<String>) -> Self {
239 self.access(access_type, &[] as &[&str], &[] as &[&str])
240 }
241
242 pub fn subject_formats(mut self, formats: Vec<String>) -> Self {
244 self.subject = Some(GnapSubjectRequest {
245 sub_id_formats: formats,
246 assertion_formats: vec![],
247 });
248 self
249 }
250
251 pub fn subject(mut self, subject: GnapSubjectRequest) -> Self {
253 self.subject = Some(subject);
254 self
255 }
256
257 pub fn build(self) -> GnapTransactionRequest {
259 GnapTransactionRequest {
260 client: self.client,
261 interact: self.interact,
262 access_token: if self.access_token.is_empty() {
263 None
264 } else {
265 Some(self.access_token)
266 },
267 subject: self.subject,
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274enum GnapTransactionState {
275 Pending,
277 Approved,
279 Denied,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284struct GnapTransaction {
285 id: String,
286 state: GnapTransactionState,
287 request: GnapTransactionRequest,
288 continue_token: String,
289 created_at: u64,
290 interact_nonce: Option<String>,
292 subject_id: Option<String>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298struct GnapIssuedToken {
299 pub value: String,
301 pub access: Vec<GnapAccessRequest>,
303 pub expires_at: u64,
305 pub key_thumbprint: Option<String>,
307 pub transaction_id: String,
309}
310
311pub struct GnapService {
312 config: GnapConfig,
313 transactions: Arc<RwLock<HashMap<String, GnapTransaction>>>,
315 issued_tokens: Arc<RwLock<HashMap<String, GnapIssuedToken>>>,
317}
318
319impl GnapService {
320 pub fn new(config: GnapConfig) -> Self {
321 Self {
322 config,
323 transactions: Arc::new(RwLock::new(HashMap::new())),
324 issued_tokens: Arc::new(RwLock::new(HashMap::new())),
325 }
326 }
327
328 fn jwk_thumbprint(jwk: &GnapJwk) -> Result<String> {
333 let canonical = match jwk.kty.as_str() {
334 "EC" => {
335 let crv = jwk.crv.as_deref().unwrap_or("");
336 let x = jwk.x.as_deref().unwrap_or("");
337 let y = jwk.y.as_deref().unwrap_or("");
338 format!(r#"{{"crv":"{crv}","kty":"EC","x":"{x}","y":"{y}"}}"#)
339 }
340 "RSA" => {
341 let e = jwk.e.as_deref().unwrap_or("");
342 let n = jwk.n.as_deref().unwrap_or("");
343 format!(r#"{{"e":"{e}","kty":"RSA","n":"{n}"}}"#)
344 }
345 "OKP" => {
346 let crv = jwk.crv.as_deref().unwrap_or("");
347 let x = jwk.x.as_deref().unwrap_or("");
348 format!(r#"{{"crv":"{crv}","kty":"OKP","x":"{x}"}}"#)
349 }
350 other => {
351 return Err(AuthError::validation(format!(
352 "Unsupported JWK key type: {other}"
353 )));
354 }
355 };
356 let hash = Sha256::digest(canonical.as_bytes());
357 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash))
358 }
359
360 #[allow(dead_code)]
367 pub fn verify_jwk_signature(jwk: &GnapJwk, message: &[u8], signature: &[u8]) -> Result<()> {
368 use base64::Engine;
369 use ring::signature;
370
371 match jwk.kty.as_str() {
372 "EC" => {
373 let crv = jwk.crv.as_deref().unwrap_or("P-256");
374 if crv != "P-256" {
375 return Err(AuthError::validation(format!(
376 "Unsupported EC curve for GNAP: {crv}"
377 )));
378 }
379 let x = base64::engine::general_purpose::URL_SAFE_NO_PAD
380 .decode(jwk.x.as_deref().unwrap_or(""))
381 .map_err(|e| AuthError::validation(format!("Invalid JWK x: {e}")))?;
382 let y = base64::engine::general_purpose::URL_SAFE_NO_PAD
383 .decode(jwk.y.as_deref().unwrap_or(""))
384 .map_err(|e| AuthError::validation(format!("Invalid JWK y: {e}")))?;
385
386 let mut pk_bytes = Vec::with_capacity(1 + x.len() + y.len());
388 pk_bytes.push(0x04);
389 pk_bytes.extend_from_slice(&x);
390 pk_bytes.extend_from_slice(&y);
391
392 let key = signature::UnparsedPublicKey::new(
393 &signature::ECDSA_P256_SHA256_ASN1,
394 &pk_bytes,
395 );
396 key.verify(message, signature).map_err(|_| {
397 AuthError::validation("GNAP client key signature verification failed (ES256)")
398 })
399 }
400 "RSA" => {
401 let n = base64::engine::general_purpose::URL_SAFE_NO_PAD
402 .decode(jwk.n.as_deref().unwrap_or(""))
403 .map_err(|e| AuthError::validation(format!("Invalid JWK n: {e}")))?;
404 let e = base64::engine::general_purpose::URL_SAFE_NO_PAD
405 .decode(jwk.e.as_deref().unwrap_or(""))
406 .map_err(|e| AuthError::validation(format!("Invalid JWK e: {e}")))?;
407
408 let pk_der = Self::encode_rsa_public_key_der(&n, &e);
410
411 let key = signature::UnparsedPublicKey::new(
412 &signature::RSA_PKCS1_2048_8192_SHA256,
413 &pk_der,
414 );
415 key.verify(message, signature).map_err(|_| {
416 AuthError::validation("GNAP client key signature verification failed (RS256)")
417 })
418 }
419 other => Err(AuthError::validation(format!(
420 "Unsupported JWK key type for signature: {other}"
421 ))),
422 }
423 }
424
425 #[allow(dead_code)]
427 fn encode_rsa_public_key_der(n: &[u8], e: &[u8]) -> Vec<u8> {
428 fn der_integer(val: &[u8]) -> Vec<u8> {
430 let v = if !val.is_empty() && val[0] == 0 {
432 let stripped = val.iter().position(|&b| b != 0).unwrap_or(val.len() - 1);
433 &val[stripped..]
434 } else {
435 val
436 };
437 let needs_pad = !v.is_empty() && v[0] & 0x80 != 0;
438 let len = v.len() + if needs_pad { 1 } else { 0 };
439 let mut out = vec![0x02]; der_encode_length(len, &mut out);
441 if needs_pad {
442 out.push(0x00);
443 }
444 out.extend_from_slice(v);
445 out
446 }
447
448 fn der_encode_length(len: usize, out: &mut Vec<u8>) {
449 if len < 0x80 {
450 out.push(len as u8);
451 } else if len < 0x100 {
452 out.push(0x81);
453 out.push(len as u8);
454 } else if len < 0x10000 {
455 out.push(0x82);
456 out.push((len >> 8) as u8);
457 out.push(len as u8);
458 } else {
459 out.push(0x83);
460 out.push((len >> 16) as u8);
461 out.push((len >> 8) as u8);
462 out.push(len as u8);
463 }
464 }
465
466 let n_der = der_integer(n);
467 let e_der = der_integer(e);
468 let rsa_seq_content_len = n_der.len() + e_der.len();
469 let mut rsa_seq = vec![0x30]; der_encode_length(rsa_seq_content_len, &mut rsa_seq);
471 rsa_seq.extend_from_slice(&n_der);
472 rsa_seq.extend_from_slice(&e_der);
473
474 let rsa_oid: &[u8] = &[
477 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
479 0x01, 0x05, 0x00, ];
482 let bitstring_len = 1 + rsa_seq.len(); let mut bitstring = vec![0x03]; der_encode_length(bitstring_len, &mut bitstring);
485 bitstring.push(0x00); bitstring.extend_from_slice(&rsa_seq);
487
488 let spki_content_len = rsa_oid.len() + bitstring.len();
489 let mut spki = vec![0x30]; der_encode_length(spki_content_len, &mut spki);
491 spki.extend_from_slice(rsa_oid);
492 spki.extend_from_slice(&bitstring);
493 spki
494 }
495
496 pub fn validate_client_key_with_proof(
505 client: &Option<GnapClientInfo>,
506 proof_message: Option<&[u8]>,
507 proof_signature: Option<&[u8]>,
508 ) -> Result<Option<String>> {
509 let client = match client {
510 Some(c) => c,
511 None => return Ok(None),
512 };
513 let key = match &client.key {
514 Some(k) => k,
515 None => return Ok(None),
516 };
517
518 match key.proof.as_str() {
520 "httpsig" | "mtls" | "dpop" | "jws" | "test" => {}
521 other => {
522 return Err(AuthError::validation(format!(
523 "Unsupported GNAP proof method: {other}"
524 )));
525 }
526 }
527
528 Self::validate_jwk_fields(&key.jwk)?;
530
531 let thumbprint = Self::jwk_thumbprint(&key.jwk)?;
532
533 if key.proof != "test" {
536 if let (Some(msg), Some(sig)) = (proof_message, proof_signature) {
537 Self::verify_jwk_signature(&key.jwk, msg, sig)?;
538 }
539 }
540
541 Ok(Some(thumbprint))
542 }
543
544 fn validate_jwk_fields(jwk: &GnapJwk) -> Result<()> {
546 match jwk.kty.as_str() {
547 "EC" => {
548 if jwk.x.is_none() || jwk.y.is_none() {
549 return Err(AuthError::validation(
550 "EC JWK must include x and y coordinates",
551 ));
552 }
553 }
554 "RSA" => {
555 if jwk.n.is_none() || jwk.e.is_none() {
556 return Err(AuthError::validation(
557 "RSA JWK must include n and e components",
558 ));
559 }
560 }
561 "OKP" => {
562 if jwk.x.is_none() {
563 return Err(AuthError::validation("OKP JWK must include x coordinate"));
564 }
565 }
566 other => {
567 return Err(AuthError::validation(format!(
568 "Unsupported JWK key type: {other}"
569 )));
570 }
571 }
572 Ok(())
573 }
574
575 fn validate_client_key(client: &Option<GnapClientInfo>) -> Result<Option<String>> {
579 let client = match client {
580 Some(c) => c,
581 None => return Ok(None),
582 };
583 let key = match &client.key {
584 Some(k) => k,
585 None => return Ok(None),
586 };
587
588 match key.proof.as_str() {
590 "httpsig" | "mtls" | "dpop" | "jws" | "test" => {}
591 other => {
592 return Err(AuthError::validation(format!(
593 "Unsupported GNAP proof method: {other}"
594 )));
595 }
596 }
597
598 Self::validate_jwk_fields(&key.jwk)?;
599 let thumbprint = Self::jwk_thumbprint(&key.jwk)?;
600 Ok(Some(thumbprint))
601 }
602
603 fn compute_interact_hash(
609 client_nonce: &str,
610 server_nonce: &str,
611 interact_ref: &str,
612 transaction_endpoint: &str,
613 ) -> String {
614 let input =
615 format!("{client_nonce}\n{server_nonce}\n{interact_ref}\n{transaction_endpoint}");
616 let hash = Sha256::digest(input.as_bytes());
617 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
618 }
619
620 fn build_access_token_response(
623 &self,
624 access_requests: &[GnapAccessRequest],
625 key_thumbprint: &Option<String>,
626 transaction_id: &str,
627 ) -> serde_json::Map<String, serde_json::Value> {
628 let access_token = uuid::Uuid::new_v4().to_string();
629 let now = std::time::SystemTime::now()
630 .duration_since(std::time::UNIX_EPOCH)
631 .unwrap_or_default()
632 .as_secs();
633
634 let issued = GnapIssuedToken {
635 value: access_token.clone(),
636 access: access_requests.to_vec(),
637 expires_at: now + self.config.token_lifetime_secs,
638 key_thumbprint: key_thumbprint.clone(),
639 transaction_id: transaction_id.to_string(),
640 };
641
642 let tokens = Arc::clone(&self.issued_tokens);
645 let token_value = access_token.clone();
646 let issued_clone = issued;
647 tokio::spawn(async move {
648 tokens.write().await.insert(token_value, issued_clone);
649 });
650
651 let mut token_obj = serde_json::Map::new();
652 token_obj.insert("value".to_string(), serde_json::Value::String(access_token));
653 token_obj.insert(
654 "expires_in".to_string(),
655 serde_json::Value::Number(self.config.token_lifetime_secs.into()),
656 );
657
658 token_obj.insert(
660 "manage".to_string(),
661 serde_json::Value::String(format!("{}/token", self.config.transaction_endpoint)),
662 );
663
664 if key_thumbprint.is_some() {
666 token_obj.insert("key".to_string(), serde_json::Value::Bool(true));
667 }
668
669 let access_json: Vec<serde_json::Value> = access_requests
670 .iter()
671 .map(|a| serde_json::to_value(a).unwrap_or_default())
672 .collect();
673 token_obj.insert("access".to_string(), serde_json::Value::Array(access_json));
674
675 token_obj
676 }
677
678 pub async fn handle_transaction(
682 &self,
683 request: GnapTransactionRequest,
684 ) -> Result<serde_json::Value> {
685 if !self.config.enabled {
686 return Err(AuthError::config("GNAP protocol is currently disabled"));
687 }
688
689 if request.access_token.is_none() && request.subject.is_none() {
691 return Err(AuthError::validation(
692 "GNAP request must include at least one of access_token or subject",
693 ));
694 }
695
696 let key_thumbprint = Self::validate_client_key(&request.client)?;
698
699 let transaction_id = uuid::Uuid::new_v4().to_string();
700 let continue_token = uuid::Uuid::new_v4().to_string();
701
702 let now = std::time::SystemTime::now()
703 .duration_since(std::time::UNIX_EPOCH)
704 .unwrap_or_default()
705 .as_secs();
706
707 let mut response = serde_json::Map::new();
708
709 let interact_nonce = if request.interact.is_some() {
711 let nonce = uuid::Uuid::new_v4().to_string();
712 Some(nonce)
713 } else {
714 None
715 };
716
717 if let Some(ref interact) = request.interact {
719 let base_url = self.config.interaction_base_url.as_deref().ok_or_else(|| {
720 AuthError::config(
721 "GNAP interaction_base_url must be configured for interactive flows",
722 )
723 })?;
724
725 let interact_url = format!("{}/interact/{}", base_url, transaction_id);
726
727 let mut interact_res = serde_json::Map::new();
728
729 if interact.start.iter().any(|m| m == "redirect") {
730 interact_res.insert(
731 "redirect".to_string(),
732 serde_json::Value::String(interact_url),
733 );
734 }
735
736 if let Some(ref finish) = interact.finish {
737 interact_res.insert(
739 "finish".to_string(),
740 serde_json::Value::String(interact_nonce.clone().unwrap_or_default()),
741 );
742 let _ = &finish.nonce; }
745
746 response.insert(
747 "interact".to_string(),
748 serde_json::Value::Object(interact_res),
749 );
750
751 let txn = GnapTransaction {
753 id: transaction_id.clone(),
754 state: GnapTransactionState::Pending,
755 request: request.clone(),
756 continue_token: continue_token.clone(),
757 created_at: now,
758 interact_nonce: interact_nonce.clone(),
759 subject_id: None,
760 };
761 self.transactions.write().await.insert(transaction_id, txn);
762 } else {
763 if let Some(ref access_requests) = request.access_token {
765 let token_obj =
766 self.build_access_token_response(access_requests, &key_thumbprint, "direct");
767 response.insert(
768 "access_token".to_string(),
769 serde_json::Value::Object(token_obj),
770 );
771 }
772
773 if let Some(ref subject_req) = request.subject {
775 let subject_resp = Self::build_subject_response(subject_req, None);
776 response.insert(
777 "subject".to_string(),
778 serde_json::Value::Object(subject_resp),
779 );
780 }
781 }
782
783 let mut continue_obj = serde_json::Map::new();
785 let mut ct_token = serde_json::Map::new();
786 ct_token.insert(
787 "value".to_string(),
788 serde_json::Value::String(continue_token),
789 );
790 continue_obj.insert(
791 "access_token".to_string(),
792 serde_json::Value::Object(ct_token),
793 );
794 continue_obj.insert(
795 "uri".to_string(),
796 serde_json::Value::String(format!("{}/continue", self.config.transaction_endpoint)),
797 );
798
799 response.insert(
800 "continue".to_string(),
801 serde_json::Value::Object(continue_obj),
802 );
803
804 Ok(serde_json::Value::Object(response))
805 }
806
807 pub async fn continue_transaction(
812 &self,
813 transaction_id: &str,
814 continue_token: &str,
815 interact_ref: Option<&str>,
816 interact_hash: Option<&str>,
817 ) -> Result<serde_json::Value> {
818 let mut transactions = self.transactions.write().await;
820 let txn = transactions
821 .get_mut(transaction_id)
822 .ok_or_else(|| AuthError::validation("Transaction not found or expired"))?;
823
824 let now = std::time::SystemTime::now()
826 .duration_since(std::time::UNIX_EPOCH)
827 .unwrap_or_default()
828 .as_secs();
829 if now.saturating_sub(txn.created_at) > self.config.transaction_lifetime_secs {
830 transactions.remove(transaction_id);
831 return Err(AuthError::validation("Transaction has expired"));
832 }
833
834 if txn.continue_token != continue_token {
836 return Err(AuthError::validation("Invalid continuation token"));
837 }
838
839 let new_continue_token = uuid::Uuid::new_v4().to_string();
841 txn.continue_token = new_continue_token.clone();
842
843 if let (Some(hash), Some(iref)) = (interact_hash, interact_ref) {
845 let server_nonce = txn.interact_nonce.as_deref().ok_or_else(|| {
846 AuthError::validation("No interaction nonce for this transaction")
847 })?;
848 let client_nonce = txn
849 .request
850 .interact
851 .as_ref()
852 .and_then(|i| i.finish.as_ref())
853 .map(|f| f.nonce.as_str())
854 .unwrap_or("");
855
856 let expected = Self::compute_interact_hash(
857 client_nonce,
858 server_nonce,
859 iref,
860 &self.config.transaction_endpoint,
861 );
862 if hash != expected {
863 return Err(AuthError::validation(
864 "Interaction hash verification failed (draft §4.2.3)",
865 ));
866 }
867 }
868
869 let result = match txn.state {
870 GnapTransactionState::Pending => {
871 let mut resp = serde_json::Map::new();
873 let mut cont = serde_json::Map::new();
874 cont.insert(
875 "uri".to_string(),
876 serde_json::Value::String(format!(
877 "{}/continue",
878 self.config.transaction_endpoint
879 )),
880 );
881 cont.insert("wait".to_string(), serde_json::Value::Number(5.into()));
882 let mut ct = serde_json::Map::new();
883 ct.insert(
884 "value".to_string(),
885 serde_json::Value::String(new_continue_token),
886 );
887 cont.insert("access_token".to_string(), serde_json::Value::Object(ct));
888 resp.insert("continue".to_string(), serde_json::Value::Object(cont));
889 Ok(serde_json::Value::Object(resp))
890 }
891 GnapTransactionState::Approved => {
892 let key_thumbprint = Self::validate_client_key(&txn.request.client).unwrap_or(None);
893
894 let mut response = serde_json::Map::new();
895
896 if let Some(ref access_requests) = txn.request.access_token {
898 let token_obj = self.build_access_token_response(
899 access_requests,
900 &key_thumbprint,
901 transaction_id,
902 );
903 response.insert(
904 "access_token".to_string(),
905 serde_json::Value::Object(token_obj),
906 );
907 }
908
909 if let Some(ref subject_req) = txn.request.subject {
911 let subject_resp =
912 Self::build_subject_response(subject_req, txn.subject_id.as_deref());
913 response.insert(
914 "subject".to_string(),
915 serde_json::Value::Object(subject_resp),
916 );
917 }
918
919 Ok(serde_json::Value::Object(response))
920 }
921 GnapTransactionState::Denied => Err(AuthError::validation(
922 "Transaction was denied by the resource owner",
923 )),
924 };
925
926 if matches!(
928 txn.state,
929 GnapTransactionState::Approved | GnapTransactionState::Denied
930 ) {
931 transactions.remove(transaction_id);
932 }
933
934 result
935 }
936
937 pub async fn approve_transaction(
940 &self,
941 transaction_id: &str,
942 subject_id: Option<&str>,
943 ) -> Result<()> {
944 let mut transactions = self.transactions.write().await;
945 let txn = transactions
946 .get_mut(transaction_id)
947 .ok_or_else(|| AuthError::validation("Transaction not found"))?;
948 txn.state = GnapTransactionState::Approved;
949 if let Some(sid) = subject_id {
950 txn.subject_id = Some(sid.to_string());
951 }
952 Ok(())
953 }
954
955 pub async fn deny_transaction(&self, transaction_id: &str) -> Result<()> {
957 let mut transactions = self.transactions.write().await;
958 let txn = transactions
959 .get_mut(transaction_id)
960 .ok_or_else(|| AuthError::validation("Transaction not found"))?;
961 txn.state = GnapTransactionState::Denied;
962 Ok(())
963 }
964
965 pub async fn revoke_token(&self, token_value: &str) -> Result<()> {
969 let mut tokens = self.issued_tokens.write().await;
970 if tokens.remove(token_value).is_none() {
971 return Err(AuthError::validation("Token not found"));
972 }
973 Ok(())
974 }
975
976 pub async fn rotate_token(&self, old_token_value: &str) -> Result<serde_json::Value> {
979 let mut tokens = self.issued_tokens.write().await;
980 let old = tokens
981 .remove(old_token_value)
982 .ok_or_else(|| AuthError::validation("Token not found or already revoked"))?;
983
984 let now = std::time::SystemTime::now()
985 .duration_since(std::time::UNIX_EPOCH)
986 .unwrap_or_default()
987 .as_secs();
988
989 if now >= old.expires_at {
990 return Err(AuthError::validation("Token has expired"));
991 }
992
993 let new_value = uuid::Uuid::new_v4().to_string();
994 let new_token = GnapIssuedToken {
995 value: new_value.clone(),
996 access: old.access.clone(),
997 expires_at: now + self.config.token_lifetime_secs,
998 key_thumbprint: old.key_thumbprint.clone(),
999 transaction_id: old.transaction_id,
1000 };
1001
1002 let mut token_obj = serde_json::Map::new();
1003 token_obj.insert(
1004 "value".to_string(),
1005 serde_json::Value::String(new_value.clone()),
1006 );
1007 token_obj.insert(
1008 "expires_in".to_string(),
1009 serde_json::Value::Number(self.config.token_lifetime_secs.into()),
1010 );
1011 token_obj.insert(
1012 "manage".to_string(),
1013 serde_json::Value::String(format!("{}/token", self.config.transaction_endpoint)),
1014 );
1015 if new_token.key_thumbprint.is_some() {
1016 token_obj.insert("key".to_string(), serde_json::Value::Bool(true));
1017 }
1018 let access_json: Vec<serde_json::Value> = old
1019 .access
1020 .iter()
1021 .map(|a| serde_json::to_value(a).unwrap_or_default())
1022 .collect();
1023 token_obj.insert("access".to_string(), serde_json::Value::Array(access_json));
1024
1025 tokens.insert(new_value, new_token);
1026 drop(tokens);
1027
1028 Ok(serde_json::Value::Object(token_obj))
1029 }
1030
1031 pub async fn introspect_token(&self, token_value: &str) -> Result<Option<serde_json::Value>> {
1033 let tokens = self.issued_tokens.read().await;
1034 let token = match tokens.get(token_value) {
1035 Some(t) => t,
1036 None => return Ok(None),
1037 };
1038
1039 let now = std::time::SystemTime::now()
1040 .duration_since(std::time::UNIX_EPOCH)
1041 .unwrap_or_default()
1042 .as_secs();
1043
1044 if now >= token.expires_at {
1045 return Ok(None);
1046 }
1047
1048 let access_json: Vec<serde_json::Value> = token
1049 .access
1050 .iter()
1051 .map(|a| serde_json::to_value(a).unwrap_or_default())
1052 .collect();
1053
1054 let mut result = serde_json::Map::new();
1055 result.insert("active".to_string(), serde_json::Value::Bool(true));
1056 result.insert("access".to_string(), serde_json::Value::Array(access_json));
1057 result.insert(
1058 "expires_in".to_string(),
1059 serde_json::Value::Number((token.expires_at - now).into()),
1060 );
1061 if let Some(ref tp) = token.key_thumbprint {
1062 result.insert(
1063 "key_thumbprint".to_string(),
1064 serde_json::Value::String(tp.clone()),
1065 );
1066 }
1067 result.insert(
1068 "key_bound".to_string(),
1069 serde_json::Value::Bool(token.key_thumbprint.is_some()),
1070 );
1071
1072 Ok(Some(serde_json::Value::Object(result)))
1073 }
1074
1075 pub async fn validate_token_key_binding(
1081 &self,
1082 token_value: &str,
1083 presenting_jwk: &GnapJwk,
1084 ) -> Result<bool> {
1085 let tokens = self.issued_tokens.read().await;
1086 let token = match tokens.get(token_value) {
1087 Some(t) => t,
1088 None => return Err(AuthError::validation("Token not found")),
1089 };
1090
1091 let now = std::time::SystemTime::now()
1092 .duration_since(std::time::UNIX_EPOCH)
1093 .unwrap_or_default()
1094 .as_secs();
1095 if now >= token.expires_at {
1096 return Err(AuthError::validation("Token has expired"));
1097 }
1098
1099 match &token.key_thumbprint {
1100 None => Ok(true), Some(expected_tp) => {
1102 let presenting_tp = Self::jwk_thumbprint(presenting_jwk)?;
1103 Ok(
1104 subtle::ConstantTimeEq::ct_eq(expected_tp.as_bytes(), presenting_tp.as_bytes())
1105 .into(),
1106 )
1107 }
1108 }
1109 }
1110
1111 fn build_subject_response(
1115 request: &GnapSubjectRequest,
1116 subject_id: Option<&str>,
1117 ) -> serde_json::Map<String, serde_json::Value> {
1118 let mut resp = serde_json::Map::new();
1119
1120 if let Some(sid) = subject_id {
1122 let mut sub_ids = Vec::new();
1123 for fmt in &request.sub_id_formats {
1124 match fmt.as_str() {
1125 "opaque" => {
1126 sub_ids.push(serde_json::json!({
1127 "format": "opaque",
1128 "id": sid,
1129 }));
1130 }
1131 "email" => {
1132 if sid.contains('@') {
1134 sub_ids.push(serde_json::json!({
1135 "format": "email",
1136 "email": sid,
1137 }));
1138 }
1139 }
1140 "iss_sub" => {
1141 sub_ids.push(serde_json::json!({
1142 "format": "iss_sub",
1143 "iss": "self",
1144 "sub": sid,
1145 }));
1146 }
1147 _ => {} }
1149 }
1150 if sub_ids.is_empty() {
1151 sub_ids.push(serde_json::json!({
1153 "format": "opaque",
1154 "id": sid,
1155 }));
1156 }
1157 resp.insert("sub_ids".to_string(), serde_json::Value::Array(sub_ids));
1158 }
1159
1160 resp
1161 }
1162
1163 pub async fn cleanup_expired_transactions(&self) {
1167 let now = std::time::SystemTime::now()
1168 .duration_since(std::time::UNIX_EPOCH)
1169 .unwrap_or_default()
1170 .as_secs();
1171 let lifetime = self.config.transaction_lifetime_secs;
1172 self.transactions
1173 .write()
1174 .await
1175 .retain(|_, t| now.saturating_sub(t.created_at) <= lifetime);
1176 }
1177
1178 pub async fn cleanup_expired_tokens(&self) {
1180 let now = std::time::SystemTime::now()
1181 .duration_since(std::time::UNIX_EPOCH)
1182 .unwrap_or_default()
1183 .as_secs();
1184 self.issued_tokens
1185 .write()
1186 .await
1187 .retain(|_, t| now < t.expires_at);
1188 }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use super::*;
1194
1195 fn test_config() -> GnapConfig {
1196 GnapConfig {
1197 enabled: true,
1198 transaction_endpoint: "/api/gnap/tx".to_string(),
1199 interaction_base_url: Some("https://auth.example.test".to_string()),
1200 token_lifetime_secs: 3600,
1201 transaction_lifetime_secs: 600,
1202 }
1203 }
1204
1205 fn test_ec_jwk() -> GnapJwk {
1206 GnapJwk {
1207 kty: "EC".to_string(),
1208 kid: Some("test-key-1".to_string()),
1209 crv: Some("P-256".to_string()),
1210 x: Some("f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU".to_string()),
1211 y: Some("x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0".to_string()),
1212 n: None,
1213 e: None,
1214 }
1215 }
1216
1217 fn test_rsa_jwk() -> GnapJwk {
1218 GnapJwk {
1219 kty: "RSA".to_string(),
1220 kid: Some("test-rsa-1".to_string()),
1221 crv: None,
1222 x: None,
1223 y: None,
1224 n: Some("0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM".to_string()),
1225 e: Some("AQAB".to_string()),
1226 }
1227 }
1228
1229 fn test_client_info(jwk: GnapJwk) -> GnapClientInfo {
1230 GnapClientInfo {
1231 key: Some(GnapClientKey {
1232 proof: "test".to_string(),
1233 jwk,
1234 }),
1235 display: Some(GnapClientDisplay {
1236 name: Some("Test Client".to_string()),
1237 uri: Some("https://client.example.test".to_string()),
1238 }),
1239 }
1240 }
1241
1242 #[test]
1245 fn test_gnap_config_defaults() {
1246 let config = GnapConfig::default();
1247 assert!(!config.enabled);
1248 assert_eq!(config.token_lifetime_secs, 3600);
1249 assert_eq!(config.transaction_lifetime_secs, 600);
1250 assert!(config.interaction_base_url.is_none());
1251 }
1252
1253 #[test]
1254 fn test_gnap_service_creation() {
1255 let service = GnapService::new(test_config());
1256 assert!(service.config.enabled);
1257 }
1258
1259 #[test]
1262 fn test_jwk_thumbprint_ec() {
1263 let jwk = test_ec_jwk();
1264 let tp = GnapService::jwk_thumbprint(&jwk).unwrap();
1265 assert!(!tp.is_empty());
1266 assert_eq!(tp.len(), 43);
1268 }
1269
1270 #[test]
1271 fn test_jwk_thumbprint_rsa() {
1272 let jwk = test_rsa_jwk();
1273 let tp = GnapService::jwk_thumbprint(&jwk).unwrap();
1274 assert!(!tp.is_empty());
1275 assert_eq!(tp.len(), 43);
1276 }
1277
1278 #[test]
1279 fn test_jwk_thumbprint_deterministic() {
1280 let jwk = test_ec_jwk();
1281 let tp1 = GnapService::jwk_thumbprint(&jwk).unwrap();
1282 let tp2 = GnapService::jwk_thumbprint(&jwk).unwrap();
1283 assert_eq!(tp1, tp2);
1284 }
1285
1286 #[test]
1287 fn test_jwk_thumbprint_different_keys() {
1288 let tp_ec = GnapService::jwk_thumbprint(&test_ec_jwk()).unwrap();
1289 let tp_rsa = GnapService::jwk_thumbprint(&test_rsa_jwk()).unwrap();
1290 assert_ne!(tp_ec, tp_rsa);
1291 }
1292
1293 #[test]
1294 fn test_jwk_thumbprint_unsupported_type() {
1295 let jwk = GnapJwk {
1296 kty: "UNKNOWN".to_string(),
1297 kid: None,
1298 crv: None,
1299 x: None,
1300 y: None,
1301 n: None,
1302 e: None,
1303 };
1304 assert!(GnapService::jwk_thumbprint(&jwk).is_err());
1305 }
1306
1307 #[test]
1310 fn test_validate_client_key_ec() {
1311 let client = Some(test_client_info(test_ec_jwk()));
1312 let tp = GnapService::validate_client_key(&client).unwrap();
1313 assert!(tp.is_some());
1314 }
1315
1316 #[test]
1317 fn test_validate_client_key_rsa() {
1318 let client = Some(test_client_info(test_rsa_jwk()));
1319 let tp = GnapService::validate_client_key(&client).unwrap();
1320 assert!(tp.is_some());
1321 }
1322
1323 #[test]
1324 fn test_validate_client_key_none() {
1325 let tp = GnapService::validate_client_key(&None).unwrap();
1326 assert!(tp.is_none());
1327 }
1328
1329 #[test]
1330 fn test_validate_client_key_no_key() {
1331 let client = Some(GnapClientInfo {
1332 key: None,
1333 display: None,
1334 });
1335 let tp = GnapService::validate_client_key(&client).unwrap();
1336 assert!(tp.is_none());
1337 }
1338
1339 #[test]
1340 fn test_validate_client_key_invalid_proof_method() {
1341 let client = Some(GnapClientInfo {
1342 key: Some(GnapClientKey {
1343 proof: "invalid_method".to_string(),
1344 jwk: test_ec_jwk(),
1345 }),
1346 display: None,
1347 });
1348 assert!(GnapService::validate_client_key(&client).is_err());
1349 }
1350
1351 #[test]
1352 fn test_validate_client_key_ec_missing_y() {
1353 let mut jwk = test_ec_jwk();
1354 jwk.y = None;
1355 let client = Some(test_client_info(jwk));
1356 assert!(GnapService::validate_client_key(&client).is_err());
1357 }
1358
1359 #[test]
1360 fn test_validate_client_key_rsa_missing_e() {
1361 let mut jwk = test_rsa_jwk();
1362 jwk.e = None;
1363 let client = Some(test_client_info(jwk));
1364 assert!(GnapService::validate_client_key(&client).is_err());
1365 }
1366
1367 #[test]
1368 fn test_validate_client_key_with_proof_test_mode() {
1369 let client = Some(test_client_info(test_ec_jwk()));
1370 let tp = GnapService::validate_client_key_with_proof(&client, None, None).unwrap();
1371 assert!(tp.is_some());
1372 }
1373
1374 #[test]
1377 fn test_compute_interact_hash_deterministic() {
1378 let h1 = GnapService::compute_interact_hash("cn1", "sn1", "ref1", "/tx");
1379 let h2 = GnapService::compute_interact_hash("cn1", "sn1", "ref1", "/tx");
1380 assert_eq!(h1, h2);
1381 }
1382
1383 #[test]
1384 fn test_compute_interact_hash_different_inputs() {
1385 let h1 = GnapService::compute_interact_hash("cn1", "sn1", "ref1", "/tx");
1386 let h2 = GnapService::compute_interact_hash("cn2", "sn1", "ref1", "/tx");
1387 assert_ne!(h1, h2);
1388 }
1389
1390 #[tokio::test]
1393 async fn test_transaction_disabled() {
1394 let mut config = test_config();
1395 config.enabled = false;
1396 let service = GnapService::new(config);
1397 let req = GnapTransactionRequest {
1398 client: None,
1399 interact: None,
1400 access_token: Some(vec![GnapAccessRequest {
1401 access_type: "read".to_string(),
1402 actions: vec![],
1403 locations: vec![],
1404 }]),
1405 subject: None,
1406 };
1407 assert!(service.handle_transaction(req).await.is_err());
1408 }
1409
1410 #[tokio::test]
1411 async fn test_transaction_requires_access_or_subject() {
1412 let service = GnapService::new(test_config());
1413 let req = GnapTransactionRequest {
1414 client: None,
1415 interact: None,
1416 access_token: None,
1417 subject: None,
1418 };
1419 assert!(service.handle_transaction(req).await.is_err());
1420 }
1421
1422 #[tokio::test]
1423 async fn test_transaction_direct_token_issuance() {
1424 let service = GnapService::new(test_config());
1425 let req = GnapTransactionRequest {
1426 client: Some(test_client_info(test_ec_jwk())),
1427 interact: None,
1428 access_token: Some(vec![GnapAccessRequest {
1429 access_type: "read".to_string(),
1430 actions: vec!["list".to_string()],
1431 locations: vec!["https://api.test/resources".to_string()],
1432 }]),
1433 subject: None,
1434 };
1435 let resp = service.handle_transaction(req).await.unwrap();
1436 let obj = resp.as_object().unwrap();
1437 assert!(obj.contains_key("access_token"));
1438 assert!(obj.contains_key("continue"));
1439
1440 let token_obj = obj["access_token"].as_object().unwrap();
1441 assert!(token_obj.contains_key("value"));
1442 assert!(token_obj.contains_key("expires_in"));
1443 assert!(token_obj.contains_key("manage"));
1444 assert_eq!(token_obj.get("key").and_then(|v| v.as_bool()), Some(true));
1445 }
1446
1447 #[tokio::test]
1448 async fn test_transaction_with_interaction() {
1449 let service = GnapService::new(test_config());
1450 let req = GnapTransactionRequest {
1451 client: Some(test_client_info(test_ec_jwk())),
1452 interact: Some(GnapInteractionRequirements {
1453 start: vec!["redirect".to_string()],
1454 finish: Some(GnapInteractionFinish {
1455 method: "redirect".to_string(),
1456 uri: "https://client.test/callback".to_string(),
1457 nonce: "client-nonce-123".to_string(),
1458 }),
1459 }),
1460 access_token: Some(vec![GnapAccessRequest {
1461 access_type: "write".to_string(),
1462 actions: vec![],
1463 locations: vec![],
1464 }]),
1465 subject: None,
1466 };
1467 let resp = service.handle_transaction(req).await.unwrap();
1468 let obj = resp.as_object().unwrap();
1469 assert!(obj.contains_key("interact"));
1470 assert!(obj.contains_key("continue"));
1471
1472 let interact = obj["interact"].as_object().unwrap();
1473 assert!(interact.contains_key("redirect"));
1474 assert!(interact.contains_key("finish"));
1475 }
1476
1477 #[tokio::test]
1478 async fn test_transaction_subject_only() {
1479 let service = GnapService::new(test_config());
1480 let req = GnapTransactionRequest {
1481 client: None,
1482 interact: None,
1483 access_token: None,
1484 subject: Some(GnapSubjectRequest {
1485 sub_id_formats: vec!["opaque".to_string()],
1486 assertion_formats: vec![],
1487 }),
1488 };
1489 let resp = service.handle_transaction(req).await.unwrap();
1490 let obj = resp.as_object().unwrap();
1491 assert!(obj.contains_key("subject"));
1492 }
1493
1494 #[tokio::test]
1497 async fn test_approve_and_continue() {
1498 let service = GnapService::new(test_config());
1499
1500 let req = GnapTransactionRequest {
1502 client: Some(test_client_info(test_ec_jwk())),
1503 interact: Some(GnapInteractionRequirements {
1504 start: vec!["redirect".to_string()],
1505 finish: None,
1506 }),
1507 access_token: Some(vec![GnapAccessRequest {
1508 access_type: "read".to_string(),
1509 actions: vec![],
1510 locations: vec![],
1511 }]),
1512 subject: None,
1513 };
1514 let resp = service.handle_transaction(req).await.unwrap();
1515 let cont = resp["continue"]["access_token"]["value"].as_str().unwrap();
1516
1517 let txn_id = {
1519 let txns = service.transactions.read().await;
1520 txns.keys().next().unwrap().clone()
1521 };
1522
1523 let poll = service
1525 .continue_transaction(&txn_id, cont, None, None)
1526 .await;
1527 assert!(
1528 poll.is_err() || {
1529 let r = poll.unwrap();
1530 r.as_object().unwrap().contains_key("continue")
1531 }
1532 );
1533
1534 let new_cont = {
1536 let txns = service.transactions.read().await;
1537 txns.get(&txn_id).unwrap().continue_token.clone()
1538 };
1539
1540 service
1542 .approve_transaction(&txn_id, Some("user-42"))
1543 .await
1544 .unwrap();
1545
1546 let result = service
1548 .continue_transaction(&txn_id, &new_cont, None, None)
1549 .await
1550 .unwrap();
1551 assert!(result.as_object().unwrap().contains_key("access_token"));
1552 }
1553
1554 #[tokio::test]
1555 async fn test_deny_transaction() {
1556 let service = GnapService::new(test_config());
1557 let req = GnapTransactionRequest {
1558 client: None,
1559 interact: Some(GnapInteractionRequirements {
1560 start: vec!["redirect".to_string()],
1561 finish: None,
1562 }),
1563 access_token: Some(vec![GnapAccessRequest {
1564 access_type: "read".to_string(),
1565 actions: vec![],
1566 locations: vec![],
1567 }]),
1568 subject: None,
1569 };
1570 service.handle_transaction(req).await.unwrap();
1571 let txn_id = {
1572 let txns = service.transactions.read().await;
1573 txns.keys().next().unwrap().clone()
1574 };
1575
1576 service.deny_transaction(&txn_id).await.unwrap();
1577
1578 let cont = {
1579 let txns = service.transactions.read().await;
1580 txns.get(&txn_id).unwrap().continue_token.clone()
1581 };
1582 let result = service
1583 .continue_transaction(&txn_id, &cont, None, None)
1584 .await;
1585 assert!(result.is_err());
1586 }
1587
1588 #[tokio::test]
1589 async fn test_continue_invalid_token() {
1590 let service = GnapService::new(test_config());
1591 let req = GnapTransactionRequest {
1592 client: None,
1593 interact: Some(GnapInteractionRequirements {
1594 start: vec!["redirect".to_string()],
1595 finish: None,
1596 }),
1597 access_token: Some(vec![GnapAccessRequest {
1598 access_type: "read".to_string(),
1599 actions: vec![],
1600 locations: vec![],
1601 }]),
1602 subject: None,
1603 };
1604 service.handle_transaction(req).await.unwrap();
1605 let txn_id = {
1606 let txns = service.transactions.read().await;
1607 txns.keys().next().unwrap().clone()
1608 };
1609 let result = service
1610 .continue_transaction(&txn_id, "bad-token", None, None)
1611 .await;
1612 assert!(result.is_err());
1613 }
1614
1615 #[tokio::test]
1618 async fn test_token_revocation() {
1619 let service = GnapService::new(test_config());
1620 let req = GnapTransactionRequest {
1621 client: None,
1622 interact: None,
1623 access_token: Some(vec![GnapAccessRequest {
1624 access_type: "read".to_string(),
1625 actions: vec![],
1626 locations: vec![],
1627 }]),
1628 subject: None,
1629 };
1630 let resp = service.handle_transaction(req).await.unwrap();
1631 let token_value = resp["access_token"]["value"].as_str().unwrap();
1632
1633 tokio::task::yield_now().await;
1635 tokio::task::yield_now().await;
1636
1637 let info = service.introspect_token(token_value).await.unwrap();
1639 assert!(info.is_some());
1640 assert_eq!(info.as_ref().unwrap()["active"], true);
1641
1642 service.revoke_token(token_value).await.unwrap();
1644
1645 let info = service.introspect_token(token_value).await.unwrap();
1647 assert!(info.is_none());
1648 }
1649
1650 #[tokio::test]
1651 async fn test_token_rotation() {
1652 let service = GnapService::new(test_config());
1653 let req = GnapTransactionRequest {
1654 client: None,
1655 interact: None,
1656 access_token: Some(vec![GnapAccessRequest {
1657 access_type: "write".to_string(),
1658 actions: vec!["create".to_string()],
1659 locations: vec![],
1660 }]),
1661 subject: None,
1662 };
1663 let resp = service.handle_transaction(req).await.unwrap();
1664 let old_token = resp["access_token"]["value"].as_str().unwrap().to_string();
1665
1666 tokio::task::yield_now().await;
1668 tokio::task::yield_now().await;
1669
1670 let new_resp = service.rotate_token(&old_token).await.unwrap();
1672 let new_token = new_resp["value"].as_str().unwrap();
1673 assert_ne!(old_token, new_token);
1674
1675 let old_info = service.introspect_token(&old_token).await.unwrap();
1677 assert!(old_info.is_none());
1678
1679 let new_info = service.introspect_token(new_token).await.unwrap();
1681 assert!(new_info.is_some());
1682 assert_eq!(new_info.unwrap()["active"], true);
1683 }
1684
1685 #[tokio::test]
1686 async fn test_revoke_nonexistent_token() {
1687 let service = GnapService::new(test_config());
1688 assert!(service.revoke_token("nonexistent").await.is_err());
1689 }
1690
1691 #[tokio::test]
1692 async fn test_introspect_nonexistent_token() {
1693 let service = GnapService::new(test_config());
1694 let info = service.introspect_token("nonexistent").await.unwrap();
1695 assert!(info.is_none());
1696 }
1697
1698 #[tokio::test]
1701 async fn test_token_key_binding_check() {
1702 let service = GnapService::new(test_config());
1703 let ec_jwk = test_ec_jwk();
1704 let req = GnapTransactionRequest {
1705 client: Some(test_client_info(ec_jwk.clone())),
1706 interact: None,
1707 access_token: Some(vec![GnapAccessRequest {
1708 access_type: "read".to_string(),
1709 actions: vec![],
1710 locations: vec![],
1711 }]),
1712 subject: None,
1713 };
1714 let resp = service.handle_transaction(req).await.unwrap();
1715 let token_value = resp["access_token"]["value"].as_str().unwrap();
1716
1717 tokio::task::yield_now().await;
1718 tokio::task::yield_now().await;
1719
1720 let ok = service
1722 .validate_token_key_binding(token_value, &ec_jwk)
1723 .await
1724 .unwrap();
1725 assert!(ok);
1726
1727 let other_jwk = test_rsa_jwk();
1729 let bad = service
1730 .validate_token_key_binding(token_value, &other_jwk)
1731 .await
1732 .unwrap();
1733 assert!(!bad);
1734 }
1735
1736 #[test]
1739 fn test_build_subject_response_opaque() {
1740 let req = GnapSubjectRequest {
1741 sub_id_formats: vec!["opaque".to_string()],
1742 assertion_formats: vec![],
1743 };
1744 let resp = GnapService::build_subject_response(&req, Some("user-42"));
1745 let sub_ids = resp["sub_ids"].as_array().unwrap();
1746 assert_eq!(sub_ids.len(), 1);
1747 assert_eq!(sub_ids[0]["format"], "opaque");
1748 assert_eq!(sub_ids[0]["id"], "user-42");
1749 }
1750
1751 #[test]
1752 fn test_build_subject_response_email() {
1753 let req = GnapSubjectRequest {
1754 sub_id_formats: vec!["email".to_string()],
1755 assertion_formats: vec![],
1756 };
1757 let resp = GnapService::build_subject_response(&req, Some("user@example.test"));
1758 let sub_ids = resp["sub_ids"].as_array().unwrap();
1759 assert_eq!(sub_ids.len(), 1);
1760 assert_eq!(sub_ids[0]["format"], "email");
1761 }
1762
1763 #[test]
1764 fn test_build_subject_response_email_not_email() {
1765 let req = GnapSubjectRequest {
1766 sub_id_formats: vec!["email".to_string()],
1767 assertion_formats: vec![],
1768 };
1769 let resp = GnapService::build_subject_response(&req, Some("user-42"));
1771 let sub_ids = resp["sub_ids"].as_array().unwrap();
1772 assert_eq!(sub_ids[0]["format"], "opaque");
1773 }
1774
1775 #[test]
1776 fn test_build_subject_response_iss_sub() {
1777 let req = GnapSubjectRequest {
1778 sub_id_formats: vec!["iss_sub".to_string()],
1779 assertion_formats: vec![],
1780 };
1781 let resp = GnapService::build_subject_response(&req, Some("user-42"));
1782 let sub_ids = resp["sub_ids"].as_array().unwrap();
1783 assert_eq!(sub_ids[0]["format"], "iss_sub");
1784 assert_eq!(sub_ids[0]["sub"], "user-42");
1785 }
1786
1787 #[test]
1788 fn test_build_subject_response_no_subject() {
1789 let req = GnapSubjectRequest {
1790 sub_id_formats: vec!["opaque".to_string()],
1791 assertion_formats: vec![],
1792 };
1793 let resp = GnapService::build_subject_response(&req, None);
1794 assert!(!resp.contains_key("sub_ids"));
1795 }
1796
1797 #[tokio::test]
1800 async fn test_continuation_token_rotates() {
1801 let service = GnapService::new(test_config());
1802 let req = GnapTransactionRequest {
1803 client: None,
1804 interact: Some(GnapInteractionRequirements {
1805 start: vec!["redirect".to_string()],
1806 finish: None,
1807 }),
1808 access_token: Some(vec![GnapAccessRequest {
1809 access_type: "read".to_string(),
1810 actions: vec![],
1811 locations: vec![],
1812 }]),
1813 subject: None,
1814 };
1815 let resp = service.handle_transaction(req).await.unwrap();
1816 let cont1 = resp["continue"]["access_token"]["value"]
1817 .as_str()
1818 .unwrap()
1819 .to_string();
1820
1821 let txn_id = {
1822 let txns = service.transactions.read().await;
1823 txns.keys().next().unwrap().clone()
1824 };
1825
1826 let _ = service
1828 .continue_transaction(&txn_id, &cont1, None, None)
1829 .await;
1830
1831 let cont2 = {
1832 let txns = service.transactions.read().await;
1833 txns.get(&txn_id).unwrap().continue_token.clone()
1834 };
1835 assert_ne!(cont1, cont2, "Continuation token must rotate on each use");
1836
1837 let reuse = service
1839 .continue_transaction(&txn_id, &cont1, None, None)
1840 .await;
1841 assert!(reuse.is_err(), "Old continuation token must be rejected");
1842 }
1843
1844 #[tokio::test]
1847 async fn test_cleanup_expired_transactions() {
1848 let mut config = test_config();
1849 config.transaction_lifetime_secs = 1; let service = GnapService::new(config);
1851
1852 let req = GnapTransactionRequest {
1853 client: None,
1854 interact: Some(GnapInteractionRequirements {
1855 start: vec!["redirect".to_string()],
1856 finish: None,
1857 }),
1858 access_token: Some(vec![GnapAccessRequest {
1859 access_type: "read".to_string(),
1860 actions: vec![],
1861 locations: vec![],
1862 }]),
1863 subject: None,
1864 };
1865 service.handle_transaction(req).await.unwrap();
1866 assert_eq!(service.transactions.read().await.len(), 1);
1867
1868 {
1870 let mut txns = service.transactions.write().await;
1871 for txn in txns.values_mut() {
1872 txn.created_at = txn.created_at.saturating_sub(10);
1873 }
1874 }
1875
1876 service.cleanup_expired_transactions().await;
1877 assert_eq!(service.transactions.read().await.len(), 0);
1878 }
1879
1880 #[tokio::test]
1883 async fn test_interaction_hash_verification() {
1884 let service = GnapService::new(test_config());
1885 let client_nonce = "client-nonce-abc";
1886 let req = GnapTransactionRequest {
1887 client: None,
1888 interact: Some(GnapInteractionRequirements {
1889 start: vec!["redirect".to_string()],
1890 finish: Some(GnapInteractionFinish {
1891 method: "redirect".to_string(),
1892 uri: "https://client.test/cb".to_string(),
1893 nonce: client_nonce.to_string(),
1894 }),
1895 }),
1896 access_token: Some(vec![GnapAccessRequest {
1897 access_type: "read".to_string(),
1898 actions: vec![],
1899 locations: vec![],
1900 }]),
1901 subject: None,
1902 };
1903 service.handle_transaction(req).await.unwrap();
1904
1905 let (txn_id, cont, server_nonce) = {
1906 let txns = service.transactions.read().await;
1907 let (id, txn) = txns.iter().next().unwrap();
1908 (
1909 id.clone(),
1910 txn.continue_token.clone(),
1911 txn.interact_nonce.clone().unwrap(),
1912 )
1913 };
1914
1915 let interact_ref = "test-interact-ref";
1916 let correct_hash = GnapService::compute_interact_hash(
1917 client_nonce,
1918 &server_nonce,
1919 interact_ref,
1920 &service.config.transaction_endpoint,
1921 );
1922
1923 let bad = service
1925 .continue_transaction(&txn_id, &cont, Some(interact_ref), Some("bad-hash"))
1926 .await;
1927 assert!(bad.is_err());
1928
1929 let fresh_cont = {
1931 let txns = service.transactions.read().await;
1932 txns.get(&txn_id).unwrap().continue_token.clone()
1933 };
1934
1935 let good = service
1937 .continue_transaction(
1938 &txn_id,
1939 &fresh_cont,
1940 Some(interact_ref),
1941 Some(&correct_hash),
1942 )
1943 .await;
1944 assert!(good.is_ok());
1945 }
1946
1947 #[test]
1948 fn test_gnap_transaction_request_builder_access() {
1949 let req = GnapTransactionRequest::builder()
1950 .access("read", &["list", "get"], &["https://api.test/resources"])
1951 .build();
1952
1953 assert!(req.client.is_none());
1954 assert!(req.interact.is_none());
1955 let access = req.access_token.unwrap();
1956 assert_eq!(access.len(), 1);
1957 assert_eq!(access[0].access_type, "read");
1958 assert_eq!(access[0].actions, vec!["list", "get"]);
1959 }
1960
1961 #[test]
1962 fn test_gnap_transaction_request_builder_full() {
1963 let jwk = test_ec_jwk();
1964 let req = GnapTransactionRequest::builder()
1965 .client_key(jwk, "test")
1966 .redirect_interaction("https://client.test/cb", "nonce-123")
1967 .access_type("write")
1968 .subject_formats(vec!["opaque".into()])
1969 .build();
1970
1971 assert!(req.client.is_some());
1972 assert!(req.interact.is_some());
1973 assert!(req.access_token.is_some());
1974 assert!(req.subject.is_some());
1975 assert_eq!(req.subject.unwrap().sub_id_formats, vec!["opaque"]);
1976 }
1977
1978 #[test]
1979 fn test_gnap_transaction_request_builder_empty() {
1980 let req = GnapTransactionRequest::builder().build();
1981 assert!(req.client.is_none());
1982 assert!(req.interact.is_none());
1983 assert!(req.access_token.is_none());
1984 assert!(req.subject.is_none());
1985 }
1986}