Skip to content

Commit d0253d1

Browse files
committed
Add troubleshooting guide links to get_token errors
1 parent fbb43b6 commit d0253d1

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},
@@ -282,5 +282,6 @@ impl TokenCredential for ClientCertificateCredential {
282282
self.cache
283283
.get_token(scopes, options, |s, o| self.get_token(s, o))
284284
.await
285+
.map_err(authentication_error::<Self>)
285286
}
286287
}

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};
@@ -164,13 +165,14 @@ impl TokenCredential for ClientSecretCredential {
164165
self.cache
165166
.get_token(scopes, options, |s, o| self.get_token_impl(s, o))
166167
.await
168+
.map_err(authentication_error::<Self>)
167169
}
168170
}
169171

170172
#[cfg(test)]
171173
mod tests {
172174
use super::*;
173-
use crate::tests::*;
175+
use crate::{tests::*, TSG_LINK};
174176
use azure_core::{
175177
authority_hosts::AZURE_PUBLIC_CLOUD,
176178
http::{headers::Headers, BufResponse, StatusCode, Transport},
@@ -234,6 +236,11 @@ mod tests {
234236
"expected error description from the response, got '{}'",
235237
err
236238
);
239+
assert!(
240+
err.to_string()
241+
.contains(&format!("{TSG_LINK}client-secret")),
242+
"expected error to contain a link to the troubleshooting guide, got '{err}'",
243+
);
237244
}
238245

239246
#[tokio::test]

sdk/identity/azure_identity/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@ fn get_authority_host(env: Option<Env>, option: Option<String>) -> Result<Url> {
112112
Url::parse(&authority_host).map_err(Into::<Error>::into)
113113
}
114114

115+
const TSG_LINK: &str = ". To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#";
116+
117+
/// Map an error from a credential's get_token() method to an ErrorKind::Credential error, appending
118+
/// a link to the troubleshooting guide entry for that credential, if it has one.
119+
///
120+
/// TODO: decide whether to map to ErrorKind::Credential here (https://github.com/Azure/azure-sdk-for-rust/issues/3127)
121+
fn authentication_error<T: 'static>(e: azure_core::Error) -> azure_core::Error {
122+
azure_core::Error::with_message_fn(e.kind().clone(), || {
123+
let type_name = std::any::type_name::<T>();
124+
let short_name = type_name.rsplit("::").next().unwrap_or(type_name); // cspell:ignore rsplit
125+
let link = match short_name {
126+
"AzureCliCredential" => format!("{TSG_LINK}azd"),
127+
"AzureDeveloperCliCredential" => format!("{TSG_LINK}azure-cli"),
128+
"AzurePipelinesCredential" => format!("{TSG_LINK}apc"),
129+
#[cfg(feature = "client_certificate")]
130+
"ClientCertificateCredential" => format!("{TSG_LINK}client-cert"),
131+
"ClientSecretCredential" => format!("{TSG_LINK}client-secret"),
132+
"ManagedIdentityCredential" => format!("{TSG_LINK}managed-id"),
133+
"WorkloadIdentityCredential" => format!("{TSG_LINK}workload"),
134+
_ => "".to_string(),
135+
};
136+
137+
format!("{short_name} authentication failed: {e}{link}")
138+
})
139+
}
140+
115141
#[test]
116142
fn test_validate_not_empty() {
117143
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

@@ -268,6 +271,56 @@ mod tests {
268271
assert!(token.expires_on > SystemTime::now());
269272
}
270273

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

0 commit comments

Comments
 (0)