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}