Skip to content

Commit facb1c7

Browse files
authored
Authentication errors include HTTP responses (#3302)
1 parent d9ad8b8 commit facb1c7

12 files changed

+391
-310
lines changed

sdk/identity/azure_identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- A `get_token()` error caused by an HTTP response carries that response. See the [troubleshooting guide](https://aka.ms/azsdk/rust/identity/troubleshoot#find-relevant-information-in-errors) for example code showing how to access the response.
8+
79
### Breaking Changes
810

911
- `ClientCertificateCredential::new()`:

sdk/identity/azure_identity/TROUBLESHOOTING.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ This error contains several pieces of information:
4242

4343
- __Correlation ID and Timestamp__: The correlation ID and timestamp identify the request in server-side logs. This information can be useful to support engineers diagnosing unexpected Microsoft Entra ID failures.
4444

45+
Many credential errors also carry the HTTP response that caused them. This can help in advanced debugging scenarios, for example when you want to check header values that aren't represented in the error message. The example below demonstrates how to access that response in such a case.
46+
47+
```rust
48+
use azure_core::error::ErrorKind;
49+
50+
let result = client.method().await;
51+
if let Err(err) = result {
52+
match err.kind() {
53+
// ErrorKind::Credential indicates an authentication problem
54+
ErrorKind::Credential => {
55+
// a credential error may wrap another error having an HTTP response
56+
if let Some(inner) = err.downcast_ref::<azure_core::Error>() {
57+
if let ErrorKind::HttpResponse {
58+
raw_response: Some(response),
59+
..
60+
} = inner.kind()
61+
{
62+
let headers = response.headers();
63+
for (name, value) in headers.iter() {
64+
println!("{}: {}", name.as_str(), value.as_str());
65+
}
66+
}
67+
}
68+
}
69+
_ => {
70+
todo!("handle other kinds of errors")
71+
}
72+
}
73+
}
74+
```
75+
4576
<a id="client-secret"></a>
4677
## Troubleshoot ClientSecretCredential authentication issues
4778

sdk/identity/azure_identity/src/azure_cli_credential.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl TokenCredential for AzureCliCredential {
161161

162162
shell_exec::<CliTokenResponse>(self.executor.clone(), &self.env, &command)
163163
.await
164-
.map_err(authentication_error::<Self>)
164+
.map_err(|err| authentication_error(stringify!(AzureCliCredential), err))
165165
}
166166
}
167167

sdk/identity/azure_identity/src/azure_developer_cli_credential.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ impl TokenCredential for AzureDeveloperCliCredential {
131131
}
132132
shell_exec::<AzdTokenResponse>(self.executor.clone(), &self.env, &command)
133133
.await
134-
.map_err(authentication_error::<Self>)
134+
.map_err(|err| authentication_error(stringify!(AzureDeveloperCliCredential), err))
135135
}
136136
}
137137

sdk/identity/azure_identity/src/azure_pipelines_credential.rs

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
// Licensed under the MIT License.
33

44
use crate::{
5-
authentication_error, env::Env, ClientAssertion, ClientAssertionCredential,
6-
ClientAssertionCredentialOptions,
5+
env::Env, ClientAssertion, ClientAssertionCredential, ClientAssertionCredentialOptions,
76
};
87
use azure_core::{
98
credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions},
@@ -109,6 +108,7 @@ impl AzurePipelinesCredential {
109108
tenant_id,
110109
client_id,
111110
client,
111+
stringify!(AzurePipelinesCredential),
112112
Some(options.credential_options),
113113
)?;
114114

@@ -124,10 +124,7 @@ impl TokenCredential for AzurePipelinesCredential {
124124
scopes: &[&str],
125125
options: Option<TokenRequestOptions<'_>>,
126126
) -> azure_core::Result<AccessToken> {
127-
self.0
128-
.get_token(scopes, options)
129-
.await
130-
.map_err(authentication_error::<Self>)
127+
self.0.get_token(scopes, options).await
131128
}
132129
}
133130

@@ -163,16 +160,19 @@ impl ClientAssertion for Client {
163160
}),
164161
)
165162
.await?;
166-
if resp.status() != StatusCode::Ok {
167-
let status_code = resp.status();
163+
let status = resp.status();
164+
if status != StatusCode::Ok {
168165
let err_headers: ErrorHeaders = resp.headers().get()?;
169-
170-
return Err(
171-
azure_core::Error::with_message(
172-
ErrorKind::HttpResponse { status: status_code, error_code: Some(status_code.canonical_reason().to_string()), raw_response: None },
173-
format!("{status_code} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}"),
174-
)
175-
);
166+
return Err(azure_core::Error::with_message(
167+
ErrorKind::HttpResponse {
168+
status,
169+
error_code: Some(status.canonical_reason().to_string()),
170+
raw_response: Some(Box::new(resp)),
171+
},
172+
format!(
173+
"{status} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}"
174+
),
175+
));
176176
}
177177

178178
let assertion: Assertion = resp.into_body().json()?;
@@ -226,9 +226,9 @@ impl fmt::Display for ErrorHeaders {
226226
#[cfg(test)]
227227
mod tests {
228228
use super::*;
229-
use crate::{env::Env, TSG_LINK_ERROR_TEXT};
229+
use crate::env::Env;
230230
use azure_core::{
231-
http::{AsyncRawResponse, ClientOptions, Transport},
231+
http::{AsyncRawResponse, ClientOptions, RawResponse, Transport},
232232
Bytes,
233233
};
234234
use azure_core_test::http::MockHttpClient;
@@ -254,24 +254,25 @@ mod tests {
254254
}
255255

256256
#[tokio::test]
257-
async fn error_headers() {
258-
let mock_client = MockHttpClient::new(|req| {
257+
async fn error_response() {
258+
let expected_status = StatusCode::Forbidden;
259+
let body = Bytes::from_static(b"content");
260+
let mut headers = Headers::new();
261+
headers.insert(MSEDGE_REF, "foo");
262+
headers.insert(VSS_E2EID, "bar");
263+
let expected_response =
264+
RawResponse::from_bytes(expected_status, headers.clone(), body.clone());
265+
let headers_for_mock = headers.clone();
266+
let body_for_mock = body.clone();
267+
let mock_client = MockHttpClient::new(move |req| {
259268
assert_eq!(
260269
req.url().as_str(),
261270
"http://localhost/get_token?api-version=7.1&serviceConnectionId=c"
262271
);
263-
let mut headers = Headers::new();
264-
headers.insert(MSEDGE_REF, "foo");
265-
headers.insert(VSS_E2EID, "bar");
272+
let headers = headers_for_mock.clone();
273+
let body = body_for_mock.clone();
266274

267-
async move {
268-
Ok(AsyncRawResponse::from_bytes(
269-
StatusCode::Forbidden,
270-
headers,
271-
Vec::new(),
272-
))
273-
}
274-
.boxed()
275+
async move { Ok(AsyncRawResponse::from_bytes(expected_status, headers, body)) }.boxed()
275276
});
276277
let options = AzurePipelinesCredentialOptions {
277278
credential_options: ClientAssertionCredentialOptions {
@@ -285,25 +286,35 @@ mod tests {
285286
&[(OIDC_VARIABLE_NAME, "http://localhost/get_token")][..],
286287
)),
287288
};
288-
let credential =
289-
AzurePipelinesCredential::new("a".into(), "b".into(), "c", "d", Some(options))
290-
.expect("valid AzurePipelinesCredential");
291-
let err = credential
289+
let err = AzurePipelinesCredential::new("a".into(), "b".into(), "c", "d", Some(options))
290+
.expect("credential")
292291
.get_token(&["default"], None)
293292
.await
294293
.expect_err("expected error");
295-
assert!(matches!(
296-
err.kind(),
297-
ErrorKind::HttpResponse { status, .. }
298-
if *status == StatusCode::Forbidden &&
299-
err.to_string().contains("foo") &&
300-
err.to_string().contains("bar"),
301-
));
302-
assert!(
303-
err.to_string()
304-
.contains(&format!("{TSG_LINK_ERROR_TEXT}#apc")),
305-
"expected error to contain a link to the troubleshooting guide, got '{err}'",
294+
295+
assert!(matches!(err.kind(), ErrorKind::Credential));
296+
assert_eq!(
297+
r#"AzurePipelinesCredential authentication failed. 403 response from the OIDC endpoint. Check service connection ID and pipeline configuration. Headers { x-msedge-ref: "foo", x-vss-e2eid: "bar" }
298+
To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#apc"#,
299+
err.to_string(),
306300
);
301+
match err
302+
.downcast_ref::<azure_core::Error>()
303+
.expect("returned error should wrap an azure_core::Error")
304+
.kind()
305+
{
306+
ErrorKind::HttpResponse {
307+
error_code: Some(reason),
308+
raw_response: Some(response),
309+
status,
310+
..
311+
} => {
312+
assert_eq!(status.canonical_reason(), reason.as_str());
313+
assert_eq!(&expected_response, response.as_ref());
314+
assert_eq!(expected_status, *status);
315+
}
316+
err => panic!("unexpected {:?}", err),
317+
};
307318
}
308319

309320
#[tokio::test]

0 commit comments

Comments
 (0)