Skip to content

Commit d7d7568

Browse files
authored
Identity cleanup (#544)
* Start clean up of errors * Fix todo * Improve device code response * More docs * Remove unnecessary traits * Fix clippy error
1 parent 87055d0 commit d7d7568

File tree

9 files changed

+159
-146
lines changed

9 files changed

+159
-146
lines changed

sdk/identity/src/authorization_code_flow.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
//!
33
//! You can learn more about the OAuth2 authorization code flow [here](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow).
44
5-
use log::debug;
65
use oauth2::basic::BasicClient;
76
use oauth2::reqwest::async_http_client;
87
use oauth2::{ClientId, ClientSecret};
@@ -93,7 +92,6 @@ impl AuthorizationCodeFlow {
9392
.request_async(async_http_client)
9493
.await?;
9594

96-
debug!("\nMS Graph returned the following token:\n{:?}\n", token);
9795
Ok(token)
9896
}
9997
}

sdk/identity/src/client_credentials_flow/login_response.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
use crate::Error;
21
use chrono::{DateTime, TimeZone, Utc};
32
use oauth2::AccessToken;
43
use serde::{de, Deserialize, Deserializer};
5-
use std::str::FromStr;
64

75
#[derive(Debug, Clone, Deserialize)]
86
struct _LoginResponse {
@@ -26,14 +24,6 @@ pub struct LoginResponse {
2624
pub access_token: AccessToken,
2725
}
2826

29-
impl FromStr for LoginResponse {
30-
type Err = Error;
31-
32-
fn from_str(s: &str) -> Result<Self, Self::Err> {
33-
Ok(serde_json::from_str(s)?)
34-
}
35-
}
36-
3727
impl<'de> Deserialize<'de> for LoginResponse {
3828
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3929
where
@@ -49,7 +39,7 @@ impl LoginResponse {
4939
&self.access_token
5040
}
5141

52-
fn from_base_response(r: _LoginResponse) -> Result<LoginResponse, Error> {
42+
fn from_base_response(r: _LoginResponse) -> Result<LoginResponse, std::num::ParseIntError> {
5343
let expires_on: Option<DateTime<Utc>> = match r.expires_on {
5444
Some(d) => Some(Utc.timestamp(d.parse()?, 0)),
5545
None => None,

sdk/identity/src/client_credentials_flow/mod.rs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Authorize using the client credentials flow
1+
//! Authorize using the OAuth 2.0 client credentials flow
22
//!
33
//! For example:
44
//!
@@ -39,7 +39,6 @@
3939
4040
mod login_response;
4141

42-
use crate::Error;
4342
use login_response::LoginResponse;
4443
use url::form_urlencoded;
4544

@@ -50,7 +49,7 @@ pub async fn perform(
5049
client_secret: &oauth2::ClientSecret,
5150
scopes: &[&str],
5251
tenant_id: &str,
53-
) -> Result<LoginResponse, Error> {
52+
) -> Result<LoginResponse, ClientCredentialError> {
5453
let encoded: String = form_urlencoded::Serializer::new(String::new())
5554
.append_pair("client_id", client_id.as_str())
5655
.append_pair("scope", &scopes.join(" "))
@@ -61,19 +60,46 @@ pub async fn perform(
6160
let url = url::Url::parse(&format!(
6261
"https://login.microsoftonline.com/{}/oauth2/v2.0/token",
6362
tenant_id
64-
))?;
63+
))
64+
.map_err(|_| ClientCredentialError::InvalidTenantId(tenant_id.to_owned()))?;
6565

66-
client
66+
let response = client
6767
.post(url)
6868
.header("ContentType", "Application / WwwFormUrlEncoded")
6969
.body(encoded)
7070
.send()
71-
.await?
71+
.await
72+
.map_err(|e| ClientCredentialError::RequestError(Box::new(e)))?;
73+
74+
if !response.status().is_success() {
75+
return Err(ClientCredentialError::UnsuccessfulResponse(
76+
response.status().as_u16(),
77+
response.text().await.ok(),
78+
));
79+
}
80+
81+
let b = response
7282
.text()
7383
.await
74-
.map(|s| -> Result<LoginResponse, Error> {
75-
Ok(serde_json::from_str::<LoginResponse>(&s)?)
76-
})?
77-
// TODO The HTTP status code should be checked to deserialize an error response.
78-
// serde_json::from_str::<crate::errors::ErrorResponse>(&s).map(Error::ErrorResponse)
84+
.map_err(|e| ClientCredentialError::RequestError(Box::new(e)))?;
85+
86+
serde_json::from_str::<LoginResponse>(&b)
87+
.map_err(|_| ClientCredentialError::InvalidResponseBody(b))
88+
}
89+
90+
/// Errors when performing the client credential flow
91+
#[derive(thiserror::Error, Debug)]
92+
pub enum ClientCredentialError {
93+
/// The http response was unsuccessful
94+
#[error("The http response was unsuccessful with status {0}: {}", .1.as_deref().unwrap_or("<NO UTF-8 BODY>"))]
95+
UnsuccessfulResponse(u16, Option<String>),
96+
/// The http response body was could not be turned into a client credential response
97+
#[error("The http response body could not be turned into a client credential response: {0}")]
98+
InvalidResponseBody(String),
99+
/// The tenant id could not be url encoded
100+
#[error("The supplied tenant id could not be url encoded: {0}")]
101+
InvalidTenantId(String),
102+
/// An error occurred when trying to make a request
103+
#[error("An error occurred when trying to make a request: {0}")]
104+
RequestError(Box<dyn std::error::Error + Send + Sync>),
79105
}

sdk/identity/src/device_code_flow/device_code_responses.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,33 @@ impl DeviceCodeAuthorization {
4444
}
4545
}
4646

47+
/// Errors when performing the device code flow
4748
#[derive(Error, Debug)]
4849
pub enum DeviceCodeError {
49-
#[error("Authorization declined")]
50+
/// The authorization response returned a "declined" response
51+
#[error("authorization declined: {0}")]
5052
AuthorizationDeclined(DeviceCodeErrorResponse),
51-
#[error("Bad verification code")]
53+
/// The authorization response returned a "bad verification" response
54+
#[error("bad verification code: {0}")]
5255
BadVerificationCode(DeviceCodeErrorResponse),
53-
#[error("Expired token")]
56+
/// The authorization response returned a "expired" response
57+
#[error("expired token: {0}")]
5458
ExpiredToken(DeviceCodeErrorResponse),
55-
#[error("Unrecognized error: {0}")]
59+
/// The authorization response returned an unrecognized error
60+
#[error("unrecognized device code error response error kind: {0}")]
5661
UnrecognizedError(DeviceCodeErrorResponse),
57-
#[error("Unhandled error: {0}. {1}")]
58-
UnhandledError(String, String),
59-
#[error("Reqwest error: {0}")]
60-
ReqwestError(reqwest::Error),
62+
/// The supplied tenant id could not be url encoded
63+
#[error("the supplied tenant id could not be url encoded: {0}")]
64+
InvalidTenantId(String),
65+
/// The HTTP response returned an unsuccessful HTTP status code
66+
#[error("the http response was unsuccesful with status {0}: {}", .1.as_deref().unwrap_or("<NO UTF-8 BODY>"))]
67+
UnsuccessfulResponse(u16, Option<String>),
68+
/// The response body could not be turned into a device code response
69+
#[error("the http response body could not be turned into a device code response: {0}")]
70+
InvalidResponseBody(String),
71+
/// An error occurred when trying to make a request
72+
#[error("an error occurred when trying to make a request: {0}")]
73+
RequestError(Box<dyn std::error::Error + Send + Sync>),
6174
}
6275

6376
#[derive(Debug, Clone)]
@@ -101,7 +114,7 @@ impl TryInto<DeviceCodeResponse> for String {
101114
}
102115
}
103116
// If we cannot, we bail out giving the full error as string
104-
Err(error) => Err(DeviceCodeError::UnhandledError(error.to_string(), self)),
117+
Err(_) => Err(DeviceCodeError::InvalidResponseBody(self)),
105118
}
106119
}
107120
}

sdk/identity/src/device_code_flow/mod.rs

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@
44
//!
55
//! You can learn more about this authorization flow [here](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code).
66
mod device_code_responses;
7-
use crate::Error;
8-
use async_timer::timer::new_timer;
7+
98
pub use device_code_responses::*;
9+
10+
use async_timer::timer::new_timer;
1011
use futures::stream::unfold;
11-
use log::debug;
1212
use oauth2::ClientId;
1313
use serde::Deserialize;
14+
use url::form_urlencoded;
15+
1416
use std::borrow::Cow;
1517
use std::convert::TryInto;
1618
use std::time::Duration;
17-
use url::form_urlencoded;
1819

1920
pub async fn start<'a, 'b, T>(
2021
client: &'a reqwest::Client,
2122
tenant_id: T,
2223
client_id: &'a ClientId,
2324
scopes: &'b [&'b str],
24-
) -> Result<DeviceCodePhaseOneResponse<'a>, Error>
25+
) -> Result<DeviceCodePhaseOneResponse<'a>, DeviceCodeError>
2526
where
2627
T: Into<Cow<'a, str>>,
2728
{
@@ -32,41 +33,48 @@ where
3233

3334
let tenant_id = tenant_id.into();
3435

35-
debug!("encoded ==> {}", encoded);
36-
3736
let url = url::Url::parse(&format!(
3837
"https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode",
3938
tenant_id
40-
))?;
39+
))
40+
.map_err(|_| DeviceCodeError::InvalidTenantId(tenant_id.clone().into_owned()))?;
4141

42-
client
42+
let response = client
4343
.post(url)
4444
.header("ContentType", "application/x-www-form-urlencoded")
4545
.body(encoded)
4646
.send()
47-
.await?
47+
.await
48+
.map_err(|e| DeviceCodeError::RequestError(Box::new(e)))?;
49+
50+
if !response.status().is_success() {
51+
return Err(DeviceCodeError::UnsuccessfulResponse(
52+
response.status().as_u16(),
53+
response.text().await.ok(),
54+
));
55+
}
56+
let s = response
4857
.text()
4958
.await
50-
.map(|s| -> Result<DeviceCodePhaseOneResponse, Error> {
51-
serde_json::from_str::<DeviceCodePhaseOneResponse>(&s)
52-
// we need to capture some variables that will be useful in
53-
// the second phase (the client, the tenant_id and the client_id)
54-
.map(|device_code_reponse| {
55-
Ok(DeviceCodePhaseOneResponse {
56-
device_code: device_code_reponse.device_code,
57-
user_code: device_code_reponse.user_code,
58-
verification_uri: device_code_reponse.verification_uri,
59-
expires_in: device_code_reponse.expires_in,
60-
interval: device_code_reponse.interval,
61-
message: device_code_reponse.message,
62-
client: Some(client),
63-
tenant_id,
64-
client_id: client_id.as_str().to_string(),
65-
})
66-
})?
67-
// TODO The HTTP status code should be checked to deserialize an error response.
68-
// serde_json::from_str::<crate::errors::ErrorResponse>(&s).map(Error::ErrorResponse)
69-
})?
59+
.map_err(|e| DeviceCodeError::RequestError(Box::new(e)))?;
60+
61+
serde_json::from_str::<DeviceCodePhaseOneResponse>(&s)
62+
// we need to capture some variables that will be useful in
63+
// the second phase (the client, the tenant_id and the client_id)
64+
.map(|device_code_reponse| {
65+
Ok(DeviceCodePhaseOneResponse {
66+
device_code: device_code_reponse.device_code,
67+
user_code: device_code_reponse.user_code,
68+
verification_uri: device_code_reponse.verification_uri,
69+
expires_in: device_code_reponse.expires_in,
70+
interval: device_code_reponse.interval,
71+
message: device_code_reponse.message,
72+
client: Some(client),
73+
tenant_id,
74+
client_id: client_id.as_str().to_string(),
75+
})
76+
})
77+
.map_err(|_| DeviceCodeError::InvalidResponseBody(s))?
7078
}
7179

7280
#[derive(Debug, Clone, Deserialize)]
@@ -77,17 +85,14 @@ pub struct DeviceCodePhaseOneResponse<'a> {
7785
expires_in: u64,
7886
interval: u64,
7987
message: String,
80-
// the skipped fields below do not come
81-
// from the Azure answer. They will be added
82-
// manually after deserialization
88+
// The skipped fields below do not come from the Azure answer.
89+
// They will be added manually after deserialization
8390
#[serde(skip)]
8491
client: Option<&'a reqwest::Client>,
8592
#[serde(skip)]
8693
tenant_id: Cow<'a, str>,
87-
// we store the ClientId as string instead of
88-
// the original type because it does not
89-
// implement Default and it's in another
90-
// create
94+
// We store the ClientId as string instead of the original type, because it
95+
// does not implement Default, and it's in another crate
9196
#[serde(skip)]
9297
client_id: String,
9398
}
@@ -97,9 +102,9 @@ impl<'a> DeviceCodePhaseOneResponse<'a> {
97102
&self.message
98103
}
99104

100-
pub fn stream<'b>(
101-
&'b self,
102-
) -> impl futures::Stream<Item = Result<DeviceCodeResponse, DeviceCodeError>> + 'b + '_ {
105+
pub fn stream(
106+
&self,
107+
) -> impl futures::Stream<Item = Result<DeviceCodeResponse, DeviceCodeError>> + '_ {
103108
#[derive(Debug, Clone, PartialEq)]
104109
enum NextState {
105110
Continue,
@@ -114,12 +119,10 @@ impl<'a> DeviceCodePhaseOneResponse<'a> {
114119
self.tenant_id,
115120
);
116121

117-
// throttle down as specified by Azure. This could be
122+
// Throttle down as specified by Azure. This could be
118123
// smarter: we could calculate the elapsed time since the
119-
// last poll and wait only the delta. For now we do not
120-
// need such precision.
124+
// last poll and wait only the delta.
121125
new_timer(Duration::from_secs(self.interval)).await;
122-
debug!("posting to {}", &uri);
123126

124127
let mut encoded = form_urlencoded::Serializer::new(String::new());
125128
let encoded = encoded
@@ -136,22 +139,24 @@ impl<'a> DeviceCodePhaseOneResponse<'a> {
136139
.body(encoded)
137140
.send()
138141
.await
139-
.map_err(DeviceCodeError::ReqwestError)
142+
.map_err(|e| DeviceCodeError::RequestError(Box::new(e)))
140143
{
141144
Ok(result) => result,
142145
Err(error) => return Some((Err(error), NextState::Finish)),
143146
};
144-
debug!("result (raw) ==> {:?}", result);
145147

146-
let result = match result.text().await.map_err(DeviceCodeError::ReqwestError) {
148+
let result = match result
149+
.text()
150+
.await
151+
.map_err(|e| DeviceCodeError::RequestError(Box::new(e)))
152+
{
147153
Ok(result) => result,
148154
Err(error) => return Some((Err(error), NextState::Finish)),
149155
};
150-
debug!("result (as text) ==> {}", result);
151156

152-
// here either we get an error response from Azure
157+
// Here either we get an error response from Azure
153158
// or we get a success. A success can be either "Pending" or
154-
// "Completed". We finish the loop only on "Completed" (ie Success)
159+
// "Completed". We finish the loop only on "Completed"
155160
match result.try_into() {
156161
Ok(device_code_response) => {
157162
let next_state = match &device_code_response {

0 commit comments

Comments
 (0)