hive_client/client/wrapper/
authentication.rs

1use crate::authentication::{ChallengeResponse, Tokens, TrustedDevice, User};
2use crate::{ApiError, AuthenticationError, Client};
3use chrono::Utc;
4use std::sync::Arc;
5
6impl Client {
7    /// Login to Hive as a User.
8    ///
9    /// This user may _optionally_ have a trusted device associated with their account.
10    ///
11    /// If provided, this induces a simpler login flow, which does not require Two Factor
12    /// Authentication ([`ChallengeResponse::SmsMfa`]).
13    ///
14    /// If not provided, a new device will be automatically confirmed with Hive during the login flow.
15    ///
16    /// # Examples
17    ///
18    /// ## Login _with_ a trusted device
19    ///
20    /// If the user has previously logged in and set the Client as a trusted device , the trusted
21    /// device can be provided to skip some authentication challenges.
22    ///
23    /// ```no_run
24    /// use hive_client::authentication::{TrustedDevice, User};
25    ///
26    /// # tokio_test::block_on(async {
27    /// let client = hive_client::Client::new("Home Automation").await;
28    ///
29    /// let trusted_device = Some(TrustedDevice::new(
30    ///     "device_password",
31    ///     "device_group_key",
32    ///     "device_key"
33    /// ));
34    ///
35    /// let attempt = client.login(User::new("example@example.com", "example", trusted_device)).await;
36    ///
37    /// // Login shouldn't require any additional challenges, as a remembered device was provided.
38    /// assert!(attempt.is_ok());
39    /// # })
40    /// ```
41    ///
42    /// ## Login _without_ a trusted device
43    ///
44    /// ```no_run
45    /// use hive_client::authentication::{ChallengeResponse, TrustedDevice, User};
46    /// use hive_client::AuthenticationError;
47    ///
48    /// # tokio_test::block_on(async {
49    /// let mut client = hive_client::Client::new("Home Automation").await;
50    ///
51    /// let attempt = client.login(User::new("example@example.com", "example", None)).await;
52    ///
53    /// match attempt {
54    ///     Ok(trusted_device) => {
55    ///        // Login was successful.
56    ///        //
57    ///        // If a trusted device has been returned this can be used to authenticate in the future.
58    ///     },
59    ///     Err(AuthenticationError::NextChallenge(challenge)) => {
60    ///        // Hive prompted for a challenge to be responded to before
61    ///        // authentication can be completed.
62    ///
63    ///        // Handle the challenge accordingly, and respond to the challenge.
64    ///        let sms_code = "123456";
65    ///        let response = client.respond_to_challenge(ChallengeResponse::SmsMfa(sms_code.to_string())).await;
66    ///
67    ///        assert!(response.is_ok());
68    ///     },
69    ///     Err(_) => {
70    ///       // Login failed, respond accordingly.
71    ///     }
72    /// }
73    /// # })
74    /// ```
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if Hive did not immediately return an active
79    /// session.
80    ///
81    /// This can happen if the credentials are invalid, or if Hive prompt for
82    /// a challenge in order to process ([`AuthenticationError::NextChallenge`]).
83    ///
84    /// In the latter case, the caller must generate a [`ChallengeResponse`] and
85    /// call [`Client::respond_to_challenge`] to continue with the authentication process.
86    pub async fn login(
87        &self,
88        user: User,
89    ) -> Result<Option<Arc<TrustedDevice>>, AuthenticationError> {
90        let mut u = self.user.lock().await;
91        let user = u.insert(user);
92
93        let (tokens, untrusted_device) = self.auth.login(user).await?;
94
95        if let Some(untrusted_device) = untrusted_device {
96            user.device.replace(Arc::new(
97                self.auth
98                    .confirm_device(
99                        &user.username,
100                        &self.friendly_name,
101                        untrusted_device,
102                        &tokens,
103                    )
104                    .await?,
105            ));
106        }
107
108        self.tokens.lock().await.replace(Arc::new(tokens));
109
110        Ok(user.device.as_ref().map(Arc::clone))
111    }
112
113    /// Respond to a challenge issued by Hive during the authentication process.
114    ///
115    /// This is typically used to handle Two Factor Authentication (2FA) challenges, but could be any
116    /// challenge issued by Hive that requires a response from the user ([`Client::login`])
117    ///
118    /// # Examples
119    ///
120    /// ```no_run
121    /// use hive_client::authentication::{ChallengeResponse, TrustedDevice, User};
122    /// use hive_client::AuthenticationError;
123    ///
124    /// # tokio_test::block_on(async {
125    /// let mut client = hive_client::Client::new("Home Automation").await;
126    ///
127    /// let attempt = client.login(User::new("example@example.com", "example", None)).await;
128    ///
129    /// match attempt {
130    ///     Ok(trusted_device) => {
131    ///         // Login was successful.
132    ///         //
133    ///         // If a trusted device has been returned this can be used to authenticate in the future.
134    ///     },
135    ///     Err(AuthenticationError::NextChallenge(challenge)) => {
136    ///         // Hive prompted for a challenge to be responded to before
137    ///         // authentication can be completed.
138    ///
139    ///         // Handle the challenge accordingly, and respond to the challenge.
140    ///         let sms_code = "123456";
141    ///         let response = client.respond_to_challenge(ChallengeResponse::SmsMfa(sms_code.to_string())).await;
142    ///
143    ///         if let Ok(trusted_device) = response {
144    ///             // Login was successful.
145    ///             //
146    ///             // If a trusted device has been returned this can be used to authenticate in the future.
147    ///         } else {
148    ///             // Challenge failed, respond accordingly.
149    ///         }
150    ///     },
151    ///     Err(_) => {
152    ///         // Login failed, respond accordingly.
153    ///     }
154    /// }
155    /// # })
156    /// ```
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the challenge submission was unsuccessful. If this
161    /// happens, the caller must check the error type and handle it accordingly.
162    pub async fn respond_to_challenge(
163        &mut self,
164        challenge_response: ChallengeResponse,
165    ) -> Result<Option<Arc<TrustedDevice>>, AuthenticationError> {
166        let mut user = self.user.lock().await;
167        let user = user.as_mut().ok_or(AuthenticationError::NotLoggedIn)?;
168
169        let (tokens, untrusted_device) = self
170            .auth
171            .respond_to_challenge(user, challenge_response)
172            .await?;
173
174        if let Some(untrusted_device) = untrusted_device {
175            user.device.replace(Arc::new(
176                self.auth
177                    .confirm_device(
178                        &user.username,
179                        &self.friendly_name,
180                        untrusted_device,
181                        &tokens,
182                    )
183                    .await?,
184            ));
185        }
186
187        self.tokens.lock().await.replace(Arc::new(tokens));
188
189        Ok(user.device.as_ref().map(Arc::clone))
190    }
191
192    /// Logout from Hive.
193    ///
194    /// Note: This only clears the client, it does not perform any operations on the Hive Account.
195    ///
196    /// # Examples
197    ///
198    /// ```no_run
199    /// use hive_client::authentication::{TrustedDevice, User};
200    ///
201    /// # tokio_test::block_on(async {
202    /// let mut client = hive_client::Client::new("Home Automation").await;
203    ///
204    /// let trusted_device = Some(TrustedDevice::new(
205    ///     "device_password",
206    ///     "device_group_key",
207    ///     "device_key"
208    /// ));
209    ///
210    /// let attempt = client.login(User::new("example@example.com", "example", trusted_device)).await;
211    ///
212    /// // Login shouldn't require any additional challenges, as a remembered device was provided.
213    /// assert!(attempt.is_ok());
214    ///
215    /// client.logout().await;
216    /// # })
217    /// ```
218    pub async fn logout(&mut self) {
219        // Note that we're not calling any operations in Cognito here. Instead,
220        // we're just dropping the tokens and user from the Client.
221        //
222        // There are a number of options for invalidating refresh tokens tokens,
223        // however the one we want is the Revoke Operation API call, which is not
224        // enabled in Hive's user pool.
225        //
226        // It's possible to use the Global Sign out endpoint, but this would sign out
227        // everyone using the same user account, which is not ideal.
228        //
229        // https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html
230        drop(self.user.lock().await.take());
231        drop(self.tokens.lock().await.take());
232
233        log::info!("Logout is complete, tokens have been dropped.");
234    }
235
236    /// Refresh the currently stored [`Tokens`], if they have expired.
237    ///
238    /// This is commonly used by wrapper API methods, before performing a call to
239    /// the Hive API, to ensure their tokens are fresh and ready to be used.
240    pub(crate) async fn refresh_tokens_if_needed(&self) -> Result<Arc<Tokens>, ApiError> {
241        let mut token_to_refresh = self.tokens.lock().await;
242
243        match token_to_refresh.as_ref() {
244            mut current_tokens
245                if current_tokens.is_some_and(|tokens| tokens.expires_at <= Utc::now()) =>
246            {
247                let current_tokens = current_tokens
248                    .take()
249                    .expect("Tokens must already be present to need to refresh");
250
251                let replacement_tokens = {
252                    let user = self.user.lock().await;
253
254                    Arc::new(
255                        self.auth
256                            .refresh_tokens(
257                                user.as_ref().and_then(|user| {
258                                    user.device
259                                        .as_ref()
260                                        .map(|device| device.device_key.as_str())
261                                }),
262                                Arc::clone(current_tokens),
263                            )
264                            .await
265                            .map_err(ApiError::AuthenticationRefreshFailed)?,
266                    )
267                };
268
269                token_to_refresh.replace(Arc::clone(&replacement_tokens));
270
271                drop(token_to_refresh);
272
273                log::info!(
274                    "Tokens have been refreshed successfully. New expiration time: {}",
275                    replacement_tokens.expires_at,
276                );
277
278                Ok(Arc::clone(&replacement_tokens))
279            }
280            Some(current_tokens) => Ok(Arc::clone(current_tokens)),
281            None => Err(ApiError::AuthenticationRefreshFailed(
282                AuthenticationError::NotLoggedIn,
283            )),
284        }
285    }
286}