Skip to main content

affinidi_did_authentication/
lib.rs

1/*!
2 * DID Authentication Library
3 *
4 * **DID Authentication steps:**
5 * Step 1. Get the challenge from a authentication service
6 * Step 2. Create a DIDComm message with the challenge in the body
7 * Step 3. Sign and Encrypt the DIDComm message, send to the authentication service
8 * Step 4. Receive the tokens from the authentication service
9 *
10 * NOTE: This library currently supports two different implementations of DID Auth
11 * 1. Affinidi Messaging
12 * 2. MeetingPlace
13 *
14 * This needs to be refactored in the future when the services align on implementation
15 */
16
17use 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/// The authorization tokens received in the fourth step of the DID authentication process
37#[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    /// Affinidi Messaging Challenge
49    Complex(HTTPResponse<DidChallenge>),
50
51    /// Affinidi MeetingPlace Challenge
52    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/// Authentication Challenge
65#[derive(Serialize, Deserialize, Debug, Default, Clone)]
66struct DidChallenge {
67    /// Challenge string from the authentication service
68    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/// The authorization tokens received in the fourth step of the DID authentication process
117#[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/// Refresh tokens response from the authentication service
126#[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/// The DID Authentication struct
146#[derive(Clone)]
147pub struct DIDAuthentication {
148    /// There are two different DID authentication methods that need to be supported for now
149    /// Set to true if
150    pub type_: AuthenticationType,
151
152    /// Authorization tokens received from the authentication service
153    pub tokens: Option<AuthorizationTokens>,
154
155    /// true if authenticated, false otherwise
156    pub authenticated: bool,
157
158    /// Custom authentication handlers
159    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    /// Set custom authentication handlers
190    pub fn with_custom_handlers(mut self, handlers: Option<CustomAuthHandlers>) -> Self {
191        self.custom_handlers = handlers;
192        self
193    }
194
195    /// Find the [serviceEndpoint](https://www.w3.org/TR/did-1.0/#services) with type `Authentication` from a DID Document
196    /// # Arguments
197    /// * `doc` - The DID Document to search
198    ///
199    /// # Returns
200    /// URI of the service endpoint if it exists
201    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    /// Authenticate with the Affinidi services
216    /// This will retry authentication if it fails
217    /// If already authenticated, short-circuits and returns immediately
218    ///
219    /// # Arguments
220    /// * `profile_did` - The DID of the profile to authenticate
221    /// * `endpoint_did` - The DID of the service endpoint to authenticate against
222    /// * `did_resolver` - The DID Resolver Cache Client
223    /// * `secrets_resolver` - The Secrets Resolver
224    /// * `client` - The HTTP Client to use for requests
225    /// * `retry_limit` - The number of times to retry authentication (-1 = unlimited)
226    ///
227    /// # Returns
228    /// Ok if successful, Err if failed
229    /// AuthorizationTokens are contained in self
230    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        // Check if custom authentication handler is provided
243        // If so, use it to authenticate
244        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        // Authenticate using default logic
259        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                // Check if we need to refresh the token
314                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            // Step 1. Get the challenge
341            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            // Step 2. Sign the challenge
360
361            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    /// Helper function to get the right endpoint address
406    /// Returns the endpoint if it's a URL, or resolves the DID to get the endpoint
407    /// # Returns
408    /// The endpoint address or a AuthenticationAbort error (hard abort)
409    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    /// Refresh the JWT access token
429    /// # Arguments
430    ///   * `refresh_token` - The refresh token to be used
431    /// # Returns
432    /// A packed DIDComm message to be sent
433    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    /// Refresh the access tokens as required
486    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                // Tokens are valid, do not need to refresh
506                Ok(())
507            }
508            RefreshCheck::Refresh => {
509                // Access token has expired, refresh it
510                // Check if custom refresh handler is provided
511                // If so, use it to refresh
512                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                // Refresh using default logic
526                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                // Access and refresh tokens have expired, need to re-authenticate
561                Err(DIDAuthError::Authentication(
562                    "Access and refresh tokens have expired".to_owned(),
563                ))
564            }
565        }
566    }
567
568    /// Creates an Affinidi Trusted Messaging Authentication Challenge Response Message
569    /// # Arguments
570    /// * `body` - The challenge body
571    /// # Returns
572    /// A DIDComm message to be sent
573    ///
574    /// Notes:
575    /// - This message will expire after 60 seconds
576    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/// Possible responses from checking authentication JWT tokens
646#[derive(PartialEq, Debug)]
647pub enum RefreshCheck {
648    /// Tokens are valid, do not need to bre refreshed
649    Ok,
650    /// Access Token has expired and needs to be refreshed
651    Refresh,
652    /// Access and Refresh Tokens have expired, need to re-authenticate from scratch
653    Expired,
654}
655
656/// Checks if the tokens need to be refreshed?
657pub 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            // Both access and refresh tokens have expired
677            RefreshCheck::Expired
678        } else {
679            // Only the access token has expired
680            RefreshCheck::Refresh
681        }
682    } else {
683        // Tokens are still valid
684        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}