Skip to main content

hive_client/client/wrapper/
authentication.rs

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