Skip to main content

hive_client/client/authentication/challenge/
mod.rs

1use crate::AuthenticationError;
2use crate::authentication::user::UntrustedDevice;
3use crate::client::authentication::{HiveAuth, Tokens};
4use aws_sdk_cognitoidentityprovider::operation::respond_to_auth_challenge::RespondToAuthChallengeOutput;
5use aws_sdk_cognitoidentityprovider::types::{
6    AuthenticationResultType, ChallengeNameType, NewDeviceMetadataType,
7};
8use std::collections::HashMap;
9use std::fmt::Debug;
10
11mod device_password_verifier;
12mod device_srp_auth;
13mod password_verifier;
14mod sms_mfa;
15
16#[derive(Debug)]
17#[non_exhaustive]
18/// The Hive authentication servers have requested a challenge be responded to before
19/// the authentication can be completed.
20pub enum ChallengeRequest {
21    /// A SMS MFA code has been sent to the user's phone number, and the user must enter it
22    /// to continue the authentication flow.
23    ///
24    /// These codes are sent to the phone number associated with the user account, and will
25    /// be six digits long.
26    SmsMfa,
27
28    /// The authentication flow has requested a password verifier challenge.
29    ///
30    /// This will be handled transparently by the crate.
31    #[doc(hidden)]
32    PasswordVerifier,
33
34    /// The authentication flow has requested an unexpected challenge which cannot be handled by
35    /// the crate.
36    Unsupported(String),
37}
38
39#[derive(Debug)]
40#[non_exhaustive]
41/// A response to a [`ChallengeRequest`] issued by the Hive authentication servers.
42pub enum ChallengeResponse {
43    /// A response to the [`ChallengeRequest::SmsMfa`] challenge, with the SMS code delivered to
44    /// the user's phone.
45    SmsMfa(String),
46    #[doc(hidden)]
47    PasswordVerifier(HashMap<String, String>),
48    #[doc(hidden)]
49    DeviceSrpAuth,
50    #[doc(hidden)]
51    DevicePasswordVerifier(HashMap<String, String>),
52}
53
54impl HiveAuth {
55    pub(crate) async fn respond_to_challenge(
56        &self,
57        challenge_response: ChallengeResponse,
58    ) -> Result<(Tokens, Option<UntrustedDevice>), AuthenticationError> {
59        let response = {
60            let mut session = self.session.write().await;
61            let session = session
62                .as_mut()
63                .ok_or(AuthenticationError::NoAuthenticationInProgress)?;
64
65            log::info!(
66                "Responding to challenge with response: {:?}",
67                &challenge_response
68            );
69
70            let response = match challenge_response {
71                ChallengeResponse::PasswordVerifier(parameters) => {
72                    password_verifier::respond_to_challenge(
73                        &self.cognito,
74                        &self.user_srp_client,
75                        self.device_srp_client.as_ref(),
76                        session,
77                        parameters,
78                    )
79                    .await?
80                }
81                ChallengeResponse::DeviceSrpAuth => {
82                    device_srp_auth::handle_challenge(
83                        &self.cognito,
84                        self.device_srp_client
85                            .as_ref()
86                            .ok_or(AuthenticationError::NoAuthenticationInProgress)?,
87                        session,
88                    )
89                    .await?
90                }
91                ChallengeResponse::DevicePasswordVerifier(parameters) => {
92                    device_password_verifier::handle_challenge(
93                        &self.cognito,
94                        self.device_srp_client
95                            .as_ref()
96                            .ok_or(AuthenticationError::NoAuthenticationInProgress)?,
97                        session,
98                        parameters,
99                    )
100                    .await?
101                }
102                ChallengeResponse::SmsMfa(code) => {
103                    sms_mfa::handle_challenge(
104                        &self.cognito,
105                        self.device_srp_client.as_ref(),
106                        session,
107                        &code,
108                    )
109                    .await?
110                }
111            };
112
113            // Update the session ID so that any subsequent calls are following the flow of the authentication
114            // challenges.
115            session.1.clone_from(&response.session);
116
117            response
118        };
119
120        self.handle_challenge_response(response).await
121    }
122
123    async fn handle_challenge_response(
124        &self,
125        response: RespondToAuthChallengeOutput,
126    ) -> Result<(Tokens, Option<UntrustedDevice>), AuthenticationError> {
127        match &response.challenge_name {
128            None => {
129                if let Some(AuthenticationResultType {
130                    id_token: Some(id_token),
131                    access_token: Some(access_token),
132                    refresh_token: Some(refresh_token),
133                    expires_in,
134                    new_device_metadata,
135                    ..
136                }) = response.authentication_result
137                {
138                    let mut untrusted_device: Option<UntrustedDevice> = None;
139                    if let Some(NewDeviceMetadataType {
140                        device_key: Some(device_key),
141                        device_group_key: Some(device_group_key),
142                        ..
143                    }) = new_device_metadata
144                    {
145                        untrusted_device =
146                            Some(UntrustedDevice::new(&device_group_key, &device_key));
147                    }
148
149                    Ok((
150                        Tokens::new(id_token, access_token, refresh_token, expires_in),
151                        untrusted_device,
152                    ))
153                } else {
154                    Err(AuthenticationError::InvalidAccessToken)
155                }
156            }
157            Some(ChallengeNameType::DeviceSrpAuth) => {
158                Box::pin(self.respond_to_challenge(ChallengeResponse::DeviceSrpAuth)).await
159            }
160            Some(ChallengeNameType::DevicePasswordVerifier) => {
161                Box::pin(
162                    self.respond_to_challenge(ChallengeResponse::DevicePasswordVerifier(
163                        response.challenge_parameters.unwrap_or_default(),
164                    )),
165                )
166                .await
167            }
168            Some(ChallengeNameType::SmsMfa) => {
169                Err(AuthenticationError::NextChallenge(ChallengeRequest::SmsMfa))
170            }
171            Some(name) => Err(AuthenticationError::UnsupportedChallenge(name.to_string())),
172        }
173    }
174}