Skip to content

Commit ff9ad68

Browse files
committed
Add troubleshooting guide links to get_token errors
1 parent 5a3c32e commit ff9ad68

File tree

6 files changed

+155
-17
lines changed

6 files changed

+155
-17
lines changed

sdk/identity/azure_identity/src/azure_pipelines_credential.rs

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

44
use crate::{
5-
env::Env, ClientAssertion, ClientAssertionCredential, ClientAssertionCredentialOptions,
5+
authentication_error, env::Env, ClientAssertion, ClientAssertionCredential,
6+
ClientAssertionCredentialOptions,
67
};
78
use azure_core::{
89
credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions},
@@ -123,7 +124,10 @@ impl TokenCredential for AzurePipelinesCredential {
123124
scopes: &[&str],
124125
options: Option<TokenRequestOptions<'_>>,
125126
) -> azure_core::Result<AccessToken> {
126-
self.0.get_token(scopes, options).await
127+
self.0
128+
.get_token(scopes, options)
129+
.await
130+
.map_err(authentication_error::<Self>)
127131
}
128132
}
129133

@@ -284,16 +288,21 @@ mod tests {
284288
let credential =
285289
AzurePipelinesCredential::new("a".into(), "b".into(), "c", "d", Some(options))
286290
.expect("valid AzurePipelinesCredential");
291+
let err = credential
292+
.get_token(&["default"], None)
293+
.await
294+
.expect_err("expected error");
287295
assert!(matches!(
288-
credential.get_token(&["default"], None).await,
289-
Err(err) if matches!(
290-
err.kind(),
291-
ErrorKind::HttpResponse { status, .. }
292-
if *status == StatusCode::Forbidden &&
293-
err.to_string().contains("foo") &&
294-
err.to_string().contains("bar"),
295-
)
296+
err.kind(),
297+
ErrorKind::HttpResponse { status, .. }
298+
if *status == StatusCode::Forbidden &&
299+
err.to_string().contains("foo") &&
300+
err.to_string().contains("bar"),
296301
));
302+
assert!(
303+
err.to_string().contains(&format!("{}apc", crate::TSG_LINK)),
304+
"expected error to contain a link to the troubleshooting guide, got '{err}'",
305+
);
297306
}
298307

299308
#[tokio::test]

sdk/identity/azure_identity/src/client_certificate_credential.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
use crate::{get_authority_host, EntraIdTokenResponse, TokenCache};
4+
use crate::{authentication_error, get_authority_host, EntraIdTokenResponse, TokenCache};
55
use azure_core::{
66
base64,
77
credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions},
@@ -275,5 +275,6 @@ impl TokenCredential for ClientCertificateCredential {
275275
self.cache
276276
.get_token(scopes, options, |s, o| self.get_token(s, o))
277277
.await
278+
.map_err(authentication_error::<Self>)
278279
}
279280
}

sdk/identity/azure_identity/src/client_secret_credential.rs

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

44
use crate::{
5-
deserialize, get_authority_host, EntraIdErrorResponse, EntraIdTokenResponse, TokenCache,
5+
authentication_error, deserialize, get_authority_host, EntraIdErrorResponse,
6+
EntraIdTokenResponse, TokenCache,
67
};
78
use azure_core::credentials::TokenRequestOptions;
89
use azure_core::http::{PipelineSendOptions, StatusCode};
@@ -159,13 +160,14 @@ impl TokenCredential for ClientSecretCredential {
159160
self.cache
160161
.get_token(scopes, options, |s, o| self.get_token_impl(s, o))
161162
.await
163+
.map_err(authentication_error::<Self>)
162164
}
163165
}
164166

165167
#[cfg(test)]
166168
mod tests {
167169
use super::*;
168-
use crate::tests::*;
170+
use crate::{tests::*, TSG_LINK};
169171
use azure_core::{
170172
http::{headers::Headers, BufResponse, StatusCode, Transport},
171173
Bytes, Result,
@@ -254,6 +256,11 @@ mod tests {
254256
"expected error description from the response, got '{}'",
255257
err
256258
);
259+
assert!(
260+
err.to_string()
261+
.contains(&format!("{TSG_LINK}client-secret")),
262+
"expected error to contain a link to the troubleshooting guide, got '{err}'",
263+
);
257264
}
258265

259266
#[tokio::test]

sdk/identity/azure_identity/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,32 @@ fn get_authority_host(env: Option<Env>, cloud: Option<&CloudConfiguration>) -> R
133133
Ok(url)
134134
}
135135

136+
const TSG_LINK: &str = ". To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#";
137+
138+
/// Map an error from a credential's get_token() method to an ErrorKind::Credential error, appending
139+
/// a link to the troubleshooting guide entry for that credential, if it has one.
140+
///
141+
/// TODO: decide whether to map to ErrorKind::Credential here (https://github.com/Azure/azure-sdk-for-rust/issues/3127)
142+
fn authentication_error<T: 'static>(e: azure_core::Error) -> azure_core::Error {
143+
azure_core::Error::with_message_fn(e.kind().clone(), || {
144+
let type_name = std::any::type_name::<T>();
145+
let short_name = type_name.rsplit("::").next().unwrap_or(type_name); // cspell:ignore rsplit
146+
let link = match short_name {
147+
"AzureCliCredential" => format!("{TSG_LINK}azd"),
148+
"AzureDeveloperCliCredential" => format!("{TSG_LINK}azure-cli"),
149+
"AzurePipelinesCredential" => format!("{TSG_LINK}apc"),
150+
#[cfg(feature = "client_certificate")]
151+
"ClientCertificateCredential" => format!("{TSG_LINK}client-cert"),
152+
"ClientSecretCredential" => format!("{TSG_LINK}client-secret"),
153+
"ManagedIdentityCredential" => format!("{TSG_LINK}managed-id"),
154+
"WorkloadIdentityCredential" => format!("{TSG_LINK}workload"),
155+
_ => "".to_string(),
156+
};
157+
158+
format!("{short_name} authentication failed: {e}{link}")
159+
})
160+
}
161+
136162
#[test]
137163
fn test_validate_not_empty() {
138164
assert!(validate_not_empty("", "it's empty").is_err());

sdk/identity/azure_identity/src/managed_identity_credential.rs

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

44
use crate::{
5-
env::Env, AppServiceManagedIdentityCredential, ImdsId, VirtualMachineManagedIdentityCredential,
5+
authentication_error, env::Env, AppServiceManagedIdentityCredential, ImdsId,
6+
VirtualMachineManagedIdentityCredential,
67
};
78
use azure_core::credentials::{AccessToken, TokenCredential, TokenRequestOptions};
89
use azure_core::http::ClientOptions;
@@ -104,7 +105,10 @@ impl TokenCredential for ManagedIdentityCredential {
104105
|| "ManagedIdentityCredential requires exactly one scope".to_string(),
105106
));
106107
}
107-
self.credential.get_token(scopes, options).await
108+
self.credential
109+
.get_token(scopes, options)
110+
.await
111+
.map_err(authentication_error::<Self>)
108112
}
109113
}
110114

@@ -423,6 +427,44 @@ mod tests {
423427
);
424428
}
425429

430+
#[tokio::test]
431+
async fn get_token_error() {
432+
let mock_client = MockHttpClient::new(|_| {
433+
async move {
434+
Ok(BufResponse::from_bytes(
435+
StatusCode::BadRequest,
436+
Headers::default(),
437+
Bytes::new(),
438+
))
439+
}
440+
.boxed()
441+
});
442+
let options = ManagedIdentityCredentialOptions {
443+
client_options: ClientOptions {
444+
transport: Some(Transport::new(Arc::new(mock_client))),
445+
..Default::default()
446+
},
447+
..Default::default()
448+
};
449+
let credential = ManagedIdentityCredential::new(Some(options)).expect("credential");
450+
let err = credential
451+
.get_token(LIVE_TEST_SCOPES, None)
452+
.await
453+
.expect_err("expected error");
454+
assert!(matches!(
455+
err.kind(),
456+
azure_core::error::ErrorKind::Credential
457+
));
458+
assert!(err
459+
.to_string()
460+
.contains("the requested identity has not been assigned to this resource"));
461+
assert!(
462+
err.to_string()
463+
.contains(&format!("{}managed-id", crate::TSG_LINK)),
464+
"expected error to contain a link to the troubleshooting guide, got '{err}'",
465+
);
466+
}
467+
426468
async fn run_imds_live_test(id: Option<UserAssignedId>) -> azure_core::Result<()> {
427469
if std::env::var("IDENTITY_IMDS_AVAILABLE").is_err() {
428470
println!("Skipped: IMDS isn't available");

sdk/identity/azure_identity/src/workload_identity_credential.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
use crate::env::Env;
4+
use crate::{authentication_error, env::Env};
55
use async_lock::{RwLock, RwLockUpgradableReadGuard};
66
use azure_core::{
77
credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions},
@@ -106,7 +106,10 @@ impl TokenCredential for WorkloadIdentityCredential {
106106
"no scopes specified",
107107
));
108108
}
109-
self.0.get_token(scopes, options).await
109+
self.0
110+
.get_token(scopes, options)
111+
.await
112+
.map_err(authentication_error::<Self>)
110113
}
111114
}
112115

@@ -270,6 +273,56 @@ mod tests {
270273
assert!(token.expires_on > SystemTime::now());
271274
}
272275

276+
#[tokio::test]
277+
async fn get_token_error() {
278+
let temp_file = TempFile::new(FAKE_ASSERTION);
279+
let description = "invalid assertion";
280+
let mock = MockSts::new(
281+
vec![BufResponse::from_bytes(
282+
StatusCode::BadRequest,
283+
Headers::default(),
284+
Bytes::from(format!(
285+
r#"{{"error":"invalid_request","error_description":"{}"}}"#,
286+
description
287+
)),
288+
)],
289+
Some(Arc::new(is_valid_request())),
290+
);
291+
let cred = WorkloadIdentityCredential::new(Some(WorkloadIdentityCredentialOptions {
292+
credential_options: ClientAssertionCredentialOptions {
293+
client_options: ClientOptions {
294+
transport: Some(Transport::new(Arc::new(mock))),
295+
..Default::default()
296+
},
297+
..Default::default()
298+
},
299+
env: Env::from(
300+
&[
301+
(AZURE_CLIENT_ID, FAKE_CLIENT_ID),
302+
(AZURE_TENANT_ID, FAKE_TENANT_ID),
303+
(AZURE_FEDERATED_TOKEN_FILE, temp_file.path.to_str().unwrap()),
304+
][..],
305+
),
306+
..Default::default()
307+
}))
308+
.expect("valid credential");
309+
310+
let err = cred
311+
.get_token(LIVE_TEST_SCOPES, None)
312+
.await
313+
.expect_err("expected error");
314+
assert!(matches!(
315+
err.kind(),
316+
azure_core::error::ErrorKind::Credential
317+
));
318+
assert!(err.to_string().contains(description));
319+
assert!(
320+
err.to_string()
321+
.contains(&format!("{}workload", crate::TSG_LINK)),
322+
"expected error to contain a link to the troubleshooting guide, got '{err}'",
323+
);
324+
}
325+
273326
#[test]
274327
fn invalid_tenant_id() {
275328
let temp_file = TempFile::new(FAKE_ASSERTION);

0 commit comments

Comments
 (0)