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