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}