hive_client/client/authentication/challenge/
mod.rs

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