1use affinidi_did_common::Document;
18use affinidi_did_resolver_cache_sdk::DIDCacheClient;
19use affinidi_messaging_didcomm::{Message, PackEncryptedOptions};
20use affinidi_secrets_resolver::SecretsResolver;
21use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
22use chrono::DateTime;
23use errors::{DIDAuthError, Result};
24use reqwest::Client;
25use serde::{Deserialize, Serialize};
26use serde_json::json;
27use std::time::SystemTime;
28use tracing::{Instrument, Level, debug, error, info, span};
29use uuid::Uuid;
30
31pub mod custom_auth;
32pub mod errors;
33
34pub use custom_auth::{CustomAuthHandler, CustomAuthHandlers, CustomRefreshHandler};
35
36#[derive(Serialize, Deserialize, Debug, Default, Clone)]
38pub struct AuthorizationTokens {
39 pub access_token: String,
40 pub access_expires_at: u64,
41 pub refresh_token: String,
42 pub refresh_expires_at: u64,
43}
44
45#[derive(Serialize, Deserialize, Debug, Clone)]
46#[serde(untagged)]
47enum DidChallenges {
48 Complex(HTTPResponse<DidChallenge>),
50
51 Simple(DidChallenge),
53}
54
55impl DidChallenges {
56 pub fn challenge(&self) -> &str {
57 match self {
58 DidChallenges::Simple(s) => &s.challenge,
59 DidChallenges::Complex(c) => &c.data.challenge,
60 }
61 }
62}
63
64#[derive(Serialize, Deserialize, Debug, Default, Clone)]
66struct DidChallenge {
67 pub challenge: String,
69}
70
71#[derive(Serialize, Deserialize, Debug, Clone)]
72#[serde(untagged)]
73enum TokensType {
74 AffinidiMessaging(HTTPResponse<AuthorizationTokens>),
75 MeetingPlace(MPAuthorizationTokens),
76}
77
78impl TokensType {
79 pub fn tokens(&self) -> Result<AuthorizationTokens> {
80 match self {
81 TokensType::AffinidiMessaging(c) => Ok(c.data.clone()),
82 TokensType::MeetingPlace(m) => {
83 let tokens = AuthorizationTokens {
84 access_token: m.access_token.clone(),
85 access_expires_at: DateTime::parse_from_rfc3339(&m.access_expires_at)
86 .map_err(|err| {
87 DIDAuthError::Authentication(format!(
88 "Invalid access_expires_at timestamp ({}): {}",
89 m.access_expires_at, err
90 ))
91 })?
92 .timestamp() as u64,
93 refresh_token: m.refresh_token.clone(),
94 refresh_expires_at: DateTime::parse_from_rfc3339(&m.refresh_expires_at)
95 .map_err(|err| {
96 DIDAuthError::Authentication(format!(
97 "Invalid refresh_expires_at timestamp ({}): {}",
98 m.access_expires_at, err
99 ))
100 })?
101 .timestamp() as u64,
102 };
103 Ok(tokens)
104 }
105 }
106 }
107}
108
109#[derive(Serialize, Deserialize, Debug, Clone)]
110struct HTTPResponse<T> {
111 #[serde(alias = "sessionId")]
112 pub session_id: String,
113 pub data: T,
114}
115
116#[derive(Serialize, Deserialize, Debug, Default, Clone)]
118pub struct MPAuthorizationTokens {
119 pub access_token: String,
120 pub access_expires_at: String,
121 pub refresh_token: String,
122 pub refresh_expires_at: String,
123}
124
125#[derive(Serialize, Deserialize, Debug, Default, Clone)]
127pub struct AuthRefreshResponse {
128 pub access_token: String,
129 pub access_expires_at: u64,
130}
131
132#[derive(Clone, Debug)]
133pub enum AuthenticationType {
134 AffinidiMessaging,
135 MeetingPlace,
136 Unknown,
137}
138
139impl AuthenticationType {
140 fn is_affinidi_messaging(&self) -> bool {
141 matches!(self, AuthenticationType::AffinidiMessaging)
142 }
143}
144
145#[derive(Clone)]
147pub struct DIDAuthentication {
148 pub type_: AuthenticationType,
151
152 pub tokens: Option<AuthorizationTokens>,
154
155 pub authenticated: bool,
157
158 pub custom_handlers: Option<CustomAuthHandlers>,
160}
161
162impl std::fmt::Debug for DIDAuthentication {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("DIDAuthentication")
165 .field("type_", &self.type_)
166 .field("tokens", &self.tokens)
167 .field("authenticated", &self.authenticated)
168 .field("custom_handlers", &self.custom_handlers.is_some())
169 .finish()
170 }
171}
172
173impl Default for DIDAuthentication {
174 fn default() -> Self {
175 Self {
176 type_: AuthenticationType::Unknown,
177 tokens: None,
178 authenticated: false,
179 custom_handlers: None,
180 }
181 }
182}
183
184impl DIDAuthentication {
185 pub fn new() -> Self {
186 Self::default()
187 }
188
189 pub fn with_custom_handlers(mut self, handlers: Option<CustomAuthHandlers>) -> Self {
191 self.custom_handlers = handlers;
192 self
193 }
194
195 pub fn find_service_endpoint(doc: &Document) -> Option<String> {
202 if let Some(service) = doc.service.iter().find(|s| {
203 if let Some(id) = &s.id {
204 id.as_str().ends_with("#auth")
205 } else {
206 false
207 }
208 }) {
209 service.service_endpoint.get_uri()
210 } else {
211 None
212 }
213 }
214
215 pub async fn authenticate<S>(
231 &mut self,
232 profile_did: &str,
233 endpoint_did: &str,
234 did_resolver: &DIDCacheClient,
235 secrets_resolver: &S,
236 client: &Client,
237 retry_limit: i32,
238 ) -> Result<()>
239 where
240 S: SecretsResolver,
241 {
242 if let Some(handlers) = &self.custom_handlers
245 && let Some(auth_handler) = &handlers.auth_handler
246 {
247 debug!("Using custom authentication handler");
248 let tokens = auth_handler
249 .authenticate(profile_did, endpoint_did, did_resolver, client)
250 .await?;
251
252 self.authenticated = true;
253 self.tokens = Some(tokens);
254 self.type_ = AuthenticationType::AffinidiMessaging;
255 return Ok(());
256 }
257
258 let mut retry_count = 0;
260 let mut timer = 1;
261 loop {
262 match self
263 ._authenticate(
264 profile_did,
265 endpoint_did,
266 did_resolver,
267 secrets_resolver,
268 client,
269 )
270 .await
271 {
272 Ok(_) => {
273 return Ok(());
274 }
275 Err(DIDAuthError::ACLDenied(err)) => {
276 return Err(DIDAuthError::ACLDenied(err));
277 }
278 Err(err) => {
279 retry_count += 1;
280 if retry_limit != -1 && retry_count >= retry_limit {
281 return Err(DIDAuthError::AuthenticationAbort(
282 "Maximum number of authentication retries reached".into(),
283 ));
284 }
285
286 error!(
287 "DID ({}): Attempt #{}. Error authenticating: {:?} :: Sleeping for ({}) seconds",
288 profile_did, retry_count, err, timer
289 );
290 tokio::time::sleep(std::time::Duration::from_secs(timer)).await;
291 if timer < 10 {
292 timer += 1;
293 }
294 }
295 }
296 }
297 }
298
299 async fn _authenticate<S>(
300 &mut self,
301 profile_did: &str,
302 endpoint_did: &str,
303 did_resolver: &DIDCacheClient,
304 secrets_resolver: &S,
305 client: &Client,
306 ) -> Result<()>
307 where
308 S: SecretsResolver,
309 {
310 let _span = span!(Level::DEBUG, "authenticate",);
311 async move {
312 if self.authenticated && self.type_.is_affinidi_messaging() {
313 match self
315 ._refresh_authentication(
316 profile_did,
317 endpoint_did,
318 did_resolver,
319 secrets_resolver,
320 client,
321 )
322 .await
323 {
324 Ok(_) => {
325 return Ok(());
326 }
327 Err(err) => {
328 error!("Error refreshing token: {:?}", err);
329 info!("Attempting to re-authenticate");
330 }
331 }
332 }
333
334 let endpoint = self
335 ._get_endpoint_address(endpoint_did, did_resolver)
336 .await?;
337
338 debug!("Retrieving authentication challenge...");
339
340 let step1_response = _http_post::<DidChallenges>(
342 client,
343 &[&endpoint, "/challenge"].concat(),
344 &format!("{{\"did\": \"{profile_did}\"}}").to_string(),
345 )
346 .await?;
347
348 match step1_response {
349 DidChallenges::Simple(_) => {
350 self.type_ = AuthenticationType::MeetingPlace;
351 }
352 DidChallenges::Complex(_) => {
353 self.type_ = AuthenticationType::AffinidiMessaging;
354 }
355 }
356
357 debug!("Challenge received:\n{:#?}", step1_response);
358
359 let auth_response =
362 self._create_auth_challenge_response(profile_did, endpoint_did, &step1_response)?;
363 debug!(
364 "Auth response message:\n{}",
365 serde_json::to_string_pretty(&auth_response).unwrap()
366 );
367
368 let (auth_msg, _) = auth_response
369 .pack_encrypted(
370 endpoint_did,
371 Some(profile_did),
372 Some(profile_did),
373 did_resolver,
374 secrets_resolver,
375 &PackEncryptedOptions::default(),
376 )
377 .await?;
378
379 debug!("Successfully packed auth message\n{:#?}", auth_msg);
380
381 let step2_body = if let DidChallenges::Complex(_) = step1_response {
382 auth_msg
383 } else {
384 json!({"challenge_response":
385 BASE64_URL_SAFE_NO_PAD.encode(&auth_msg)
386 })
387 .to_string()
388 };
389
390 let step2_response =
391 _http_post::<TokensType>(client, &[&endpoint, ""].concat(), &step2_body).await?;
392
393 debug!("Tokens received:\n{:#?}", step2_response);
394
395 debug!("Successfully authenticated");
396
397 self.authenticated = true;
398 self.tokens = Some(step2_response.tokens()?);
399 Ok(())
400 }
401 .instrument(_span)
402 .await
403 }
404
405 async fn _get_endpoint_address(
410 &self,
411 endpoint_did: &str,
412 did_resolver: &DIDCacheClient,
413 ) -> Result<String> {
414 if endpoint_did.starts_with("did:") {
415 let doc = did_resolver.resolve(endpoint_did).await?;
416 if let Some(endpoint) = DIDAuthentication::find_service_endpoint(&doc.doc) {
417 Ok(endpoint)
418 } else {
419 Err(DIDAuthError::AuthenticationAbort(
420 "No service endpoint found. DID doesn't contain a #auth service".into(),
421 ))
422 }
423 } else {
424 Ok(endpoint_did.to_string())
425 }
426 }
427
428 async fn _create_refresh_request<S>(
434 &self,
435 profile_did: &str,
436 endpoint_did: &str,
437 did_resolver: &DIDCacheClient,
438 secrets_resolver: &S,
439 ) -> Result<String>
440 where
441 S: SecretsResolver,
442 {
443 let refresh_token = if let Some(tokens) = &self.tokens {
444 &tokens.refresh_token
445 } else {
446 return Err(DIDAuthError::Authentication(
447 "No tokens found to refresh".to_owned(),
448 ));
449 };
450
451 let now = SystemTime::now()
452 .duration_since(SystemTime::UNIX_EPOCH)
453 .unwrap()
454 .as_secs();
455
456 let refresh_message = Message::build(
457 Uuid::new_v4().into(),
458 "https://affinidi.com/atm/1.0/authenticate/refresh".to_string(),
459 json!({"refresh_token": refresh_token}),
460 )
461 .to(endpoint_did.to_string())
462 .from(profile_did.to_owned())
463 .created_time(now)
464 .expires_time(now + 60)
465 .finalize();
466
467 match refresh_message
468 .pack_encrypted(
469 endpoint_did,
470 Some(profile_did),
471 Some(profile_did),
472 did_resolver,
473 secrets_resolver,
474 &PackEncryptedOptions::default(),
475 )
476 .await
477 {
478 Ok((refresh_msg, _)) => Ok(refresh_msg),
479 Err(err) => Err(DIDAuthError::Authentication(format!(
480 "Couldn't pack authentication refresh message: {err:?}"
481 ))),
482 }
483 }
484
485 async fn _refresh_authentication<S>(
487 &mut self,
488 profile_did: &str,
489 endpoint_did: &str,
490 did_resolver: &DIDCacheClient,
491 secrets_resolver: &S,
492 client: &Client,
493 ) -> Result<()>
494 where
495 S: SecretsResolver,
496 {
497 let Some(tokens) = &self.tokens else {
498 return Err(DIDAuthError::Authentication(
499 "No tokens found to refresh".to_owned(),
500 ));
501 };
502
503 match refresh_check(tokens) {
504 RefreshCheck::Ok => {
505 Ok(())
507 }
508 RefreshCheck::Refresh => {
509 if let Some(handlers) = &self.custom_handlers
513 && let Some(refresh_handler) = &handlers.refresh_handler
514 {
515 debug!("Using custom refresh handler");
516 let new_tokens = refresh_handler
517 .refresh(profile_did, endpoint_did, tokens, did_resolver, client)
518 .await?;
519
520 self.tokens = Some(new_tokens);
521 debug!("JWT successfully refreshed using custom handler");
522 return Ok(());
523 }
524
525 debug!("Refreshing tokens");
527 let refresh_msg = self
528 ._create_refresh_request(
529 profile_did,
530 endpoint_did,
531 did_resolver,
532 secrets_resolver,
533 )
534 .await?;
535
536 let endpoint = self
537 ._get_endpoint_address(endpoint_did, did_resolver)
538 .await?;
539
540 let new_tokens = _http_post::<HTTPResponse<AuthRefreshResponse>>(
541 client,
542 &[&endpoint, "/refresh"].concat(),
543 &refresh_msg,
544 )
545 .await?;
546
547 let Some(tokens) = &mut self.tokens else {
548 return Err(DIDAuthError::Authentication(
549 "No tokens found to refresh".to_owned(),
550 ));
551 };
552
553 tokens.access_token = new_tokens.data.access_token;
554 tokens.access_expires_at = new_tokens.data.access_expires_at;
555
556 debug!("JWT successfully refreshed");
557 Ok(())
558 }
559 RefreshCheck::Expired => {
560 Err(DIDAuthError::Authentication(
562 "Access and refresh tokens have expired".to_owned(),
563 ))
564 }
565 }
566 }
567
568 fn _create_auth_challenge_response(
577 &self,
578 profile_did: &str,
579 endpoint_did: &str,
580 body: &DidChallenges,
581 ) -> Result<Message> {
582 let now = SystemTime::now()
583 .duration_since(SystemTime::UNIX_EPOCH)
584 .unwrap()
585 .as_secs();
586
587 let body = if let DidChallenges::Complex(c) = body {
588 json!({"challenge": c.data.challenge, "session_id": c.session_id})
589 } else {
590 json!({"challenge": body.challenge()})
591 };
592
593 Ok(Message::build(
594 Uuid::new_v4().into(),
595 "https://affinidi.com/atm/1.0/authenticate".to_owned(),
596 body,
597 )
598 .to(endpoint_did.to_string())
599 .from(profile_did.to_owned())
600 .created_time(now)
601 .expires_time(now + 60)
602 .finalize())
603 }
604}
605
606async fn _http_post<T>(client: &Client, url: &str, body: &str) -> Result<T>
607where
608 T: for<'de> Deserialize<'de>,
609{
610 debug!("POSTing to {}", url);
611 debug!("Body: {}", body);
612 let response = client
613 .post(url)
614 .header("Content-Type", "application/json")
615 .body(body.to_string())
616 .send()
617 .await
618 .map_err(|e| DIDAuthError::Authentication(format!("HTTP POST failed ({url}): {e:?}")))?;
619
620 let response_status = response.status();
621 let response_body = response
622 .text()
623 .await
624 .map_err(|e| DIDAuthError::Authentication(format!("Couldn't get HTTP body: {e:?}")))?;
625
626 debug!(
627 "status: {} response body: {}",
628 response_status, response_body
629 );
630 if !response_status.is_success() {
631 if response_status.as_u16() == 401 {
632 return Err(DIDAuthError::ACLDenied("Authentication Denied".into()));
633 } else {
634 return Err(DIDAuthError::Authentication(format!(
635 "Failed to get authentication response. url: {url}, status: {response_status}"
636 )));
637 }
638 }
639
640 serde_json::from_str::<T>(&response_body).map_err(|e| {
641 DIDAuthError::Authentication(format!("Couldn't deserialize AuthorizationResponse: {e}"))
642 })
643}
644
645#[derive(PartialEq, Debug)]
647pub enum RefreshCheck {
648 Ok,
650 Refresh,
652 Expired,
654}
655
656pub fn refresh_check(tokens: &AuthorizationTokens) -> RefreshCheck {
658 let now = SystemTime::now()
659 .duration_since(SystemTime::UNIX_EPOCH)
660 .unwrap()
661 .as_secs();
662
663 debug!(
664 "checking auth expiry: now({}), access_expires_at({}), delta({}), expired?({}), refresh_expires_at({}), delta({}), expired?({})",
665 now,
666 tokens.access_expires_at,
667 tokens.access_expires_at as i64 - now as i64,
668 tokens.access_expires_at - 5 <= now,
669 tokens.refresh_expires_at,
670 tokens.refresh_expires_at as i64 - now as i64,
671 tokens.refresh_expires_at <= now
672 );
673
674 if tokens.access_expires_at - 5 <= now {
675 if tokens.refresh_expires_at <= now {
676 RefreshCheck::Expired
678 } else {
679 RefreshCheck::Refresh
681 }
682 } else {
683 RefreshCheck::Ok
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use crate::{AuthorizationTokens, RefreshCheck, refresh_check};
691 use std::time::SystemTime;
692
693 #[test]
694 fn refresh_check_valid() {
695 let now = SystemTime::now()
696 .duration_since(SystemTime::UNIX_EPOCH)
697 .unwrap()
698 .as_secs();
699 let tokens = AuthorizationTokens {
700 access_expires_at: now + 900,
701 refresh_expires_at: now + 1800,
702 ..Default::default()
703 };
704
705 assert_eq!(refresh_check(&tokens), RefreshCheck::Ok);
706 }
707
708 #[test]
709 fn refresh_check_refresh() {
710 let now = SystemTime::now()
711 .duration_since(SystemTime::UNIX_EPOCH)
712 .unwrap()
713 .as_secs();
714 let tokens = AuthorizationTokens {
715 access_expires_at: now,
716 refresh_expires_at: now + 1800,
717 ..Default::default()
718 };
719
720 assert_eq!(refresh_check(&tokens), RefreshCheck::Refresh);
721 }
722
723 #[test]
724 fn refresh_check_expired() {
725 let now = SystemTime::now()
726 .duration_since(SystemTime::UNIX_EPOCH)
727 .unwrap()
728 .as_secs();
729 let tokens = AuthorizationTokens {
730 access_expires_at: now,
731 refresh_expires_at: now,
732 ..Default::default()
733 };
734
735 assert_eq!(refresh_check(&tokens), RefreshCheck::Expired);
736 }
737}