From 500996f6ddeebcddb35f0d6cf769a23f8bbcfbb6 Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:17:41 +0000 Subject: [PATCH 1/7] Authentication errors include HTTP responses --- sdk/identity/azure_identity/CHANGELOG.md | 2 + .../azure_identity/TROUBLESHOOTING.md | 33 +++++ .../src/azure_pipelines_credential.rs | 91 +++++++------ .../src/client_assertion_credential.rs | 85 ++++++------ .../src/client_certificate_credential.rs | 79 +++++------- .../src/client_secret_credential.rs | 85 +++++------- .../src/imds_managed_identity_credential.rs | 30 ++--- sdk/identity/azure_identity/src/lib.rs | 111 +++++++++++----- .../src/managed_identity_credential.rs | 121 +++++++++++------- .../src/workload_identity_credential.rs | 50 +++++--- 10 files changed, 395 insertions(+), 292 deletions(-) diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md index a96f377ca7..8bd975672c 100644 --- a/sdk/identity/azure_identity/CHANGELOG.md +++ b/sdk/identity/azure_identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- A `get_token()` error motivated by an HTTP response carries that response. See the [troubleshooting guide](http://aka.ms/azsdk/rust/identity/troubleshoot#find-relevant-information-in-errors) for example code showing how to access the response. + ### Breaking Changes - `ClientCertificateCredential::new()`: diff --git a/sdk/identity/azure_identity/TROUBLESHOOTING.md b/sdk/identity/azure_identity/TROUBLESHOOTING.md index 29ee09420c..3673923382 100644 --- a/sdk/identity/azure_identity/TROUBLESHOOTING.md +++ b/sdk/identity/azure_identity/TROUBLESHOOTING.md @@ -42,6 +42,39 @@ This error contains several pieces of information: - __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. +Many credential errors also carry the HTTP response that motivated them. The example below demonstrates how to access that response in such a case. + +```rust +use azure_core::error::ErrorKind; + +let result = client.method().await; +if let Err(err) = result { + match err.kind() { + // ErrorKind::Credential indicates an authentication problem + ErrorKind::Credential => { + // a credential error may wrap another error having an HTTP response + if let Some(inner) = err.downcast_ref::() { + if let ErrorKind::HttpResponse { + raw_response: Some(response), + status, + .. + } = inner.kind() + { + let headers = response.headers(); + let body = String::from_utf8_lossy(response.body()); + eprintln!("status: {status}"); + eprintln!("headers: {headers:?}"); + eprintln!("body: {body}"); + } + } + } + _ => { + // TODO: handle other kinds of error + } + } +} +``` + ## Troubleshoot ClientSecretCredential authentication issues diff --git a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs index ea0471a87a..b673e1091c 100644 --- a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs +++ b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs @@ -163,16 +163,20 @@ impl ClientAssertion for Client { }), ) .await?; - if resp.status() != StatusCode::Ok { - let status_code = resp.status(); + let status = resp.status(); + if status != StatusCode::Ok { let err_headers: ErrorHeaders = resp.headers().get()?; - - return Err( - azure_core::Error::with_message( - ErrorKind::HttpResponse { status: status_code, error_code: Some(status_code.canonical_reason().to_string()), raw_response: None }, - format!("{status_code} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}"), - ) + let message = format!( + "{status} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}" ); + return Err(azure_core::Error::with_message( + ErrorKind::HttpResponse { + status, + error_code: Some(status.canonical_reason().to_string()), + raw_response: Some(Box::new(resp)), + }, + message, + )); } let assertion: Assertion = resp.into_body().json()?; @@ -226,9 +230,9 @@ impl fmt::Display for ErrorHeaders { #[cfg(test)] mod tests { use super::*; - use crate::{env::Env, TSG_LINK_ERROR_TEXT}; + use crate::env::Env; use azure_core::{ - http::{AsyncRawResponse, ClientOptions, Transport}, + http::{AsyncRawResponse, ClientOptions, RawResponse, Transport}, Bytes, }; use azure_core_test::http::MockHttpClient; @@ -254,24 +258,25 @@ mod tests { } #[tokio::test] - async fn error_headers() { - let mock_client = MockHttpClient::new(|req| { + async fn error_response() { + let expected_status = StatusCode::Forbidden; + let body = Bytes::from_static(b"content"); + let mut headers = Headers::new(); + headers.insert(MSEDGE_REF, "foo"); + headers.insert(VSS_E2EID, "bar"); + let expected_response = + RawResponse::from_bytes(expected_status, headers.clone(), body.clone()); + let headers_for_mock = headers.clone(); + let body_for_mock = body.clone(); + let mock_client = MockHttpClient::new(move |req| { assert_eq!( req.url().as_str(), "http://localhost/get_token?api-version=7.1&serviceConnectionId=c" ); - let mut headers = Headers::new(); - headers.insert(MSEDGE_REF, "foo"); - headers.insert(VSS_E2EID, "bar"); + let headers = headers_for_mock.clone(); + let body = body_for_mock.clone(); - async move { - Ok(AsyncRawResponse::from_bytes( - StatusCode::Forbidden, - headers, - Vec::new(), - )) - } - .boxed() + async move { Ok(AsyncRawResponse::from_bytes(expected_status, headers, body)) }.boxed() }); let options = AzurePipelinesCredentialOptions { credential_options: ClientAssertionCredentialOptions { @@ -285,25 +290,35 @@ mod tests { &[(OIDC_VARIABLE_NAME, "http://localhost/get_token")][..], )), }; - let credential = - AzurePipelinesCredential::new("a".into(), "b".into(), "c", "d", Some(options)) - .expect("valid AzurePipelinesCredential"); - let err = credential + let err = AzurePipelinesCredential::new("a".into(), "b".into(), "c", "d", Some(options)) + .expect("credential") .get_token(&["default"], None) .await .expect_err("expected error"); - assert!(matches!( - err.kind(), - ErrorKind::HttpResponse { status, .. } - if *status == StatusCode::Forbidden && - err.to_string().contains("foo") && - err.to_string().contains("bar"), - )); - assert!( - err.to_string() - .contains(&format!("{TSG_LINK_ERROR_TEXT}#apc")), - "expected error to contain a link to the troubleshooting guide, got '{err}'", + + assert!(matches!(err.kind(), ErrorKind::Credential)); + assert_eq!( + 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" } +To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#apc"#, + err.to_string(), ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: Some(reason), + raw_response: Some(response), + status, + .. + } => { + assert_eq!(status.canonical_reason(), reason.as_str()); + assert_eq!(&expected_response, response.as_ref()); + assert_eq!(expected_status, *status); + } + err => panic!("unexpected {:?}", err), + }; } #[tokio::test] diff --git a/sdk/identity/azure_identity/src/client_assertion_credential.rs b/sdk/identity/azure_identity/src/client_assertion_credential.rs index 717f7b9827..fda72134da 100644 --- a/sdk/identity/azure_identity/src/client_assertion_credential.rs +++ b/sdk/identity/azure_identity/src/client_assertion_credential.rs @@ -1,26 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::{ - deserialize, get_authority_host, validate_not_empty, validate_tenant_id, EntraIdErrorResponse, - EntraIdTokenResponse, TokenCache, -}; +use crate::{get_authority_host, validate_not_empty, validate_tenant_id, TokenCache}; use azure_core::{ credentials::{AccessToken, TokenCredential, TokenRequestOptions}, error::{ErrorKind, ResultExt}, http::{ headers::{self, content_type}, - ClientMethodOptions, ClientOptions, Method, Pipeline, PipelineSendOptions, Request, - StatusCode, Url, + ClientMethodOptions, ClientOptions, Method, Pipeline, PipelineSendOptions, Request, Url, }, - time::{Duration, OffsetDateTime}, - Error, }; use std::{fmt::Debug, str, sync::Arc}; use url::form_urlencoded; const ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; -const CLIENT_ASSERTION_CREDENTIAL: &str = "ClientAssertionCredential"; /// Enables authentication of a Microsoft Entra service principal using a signed client assertion. #[derive(Debug)] @@ -144,29 +137,7 @@ impl ClientAssertionCredential { ) .await?; - match res.status() { - StatusCode::Ok => { - let token_response: EntraIdTokenResponse = - deserialize(CLIENT_ASSERTION_CREDENTIAL, res)?; - Ok(AccessToken::new( - token_response.access_token, - OffsetDateTime::now_utc() + Duration::seconds(token_response.expires_in), - )) - } - _ => { - let error_response: EntraIdErrorResponse = - deserialize(CLIENT_ASSERTION_CREDENTIAL, res)?; - let message = if error_response.error_description.is_empty() { - format!("{} authentication failed.", CLIENT_ASSERTION_CREDENTIAL) - } else { - format!( - "{} authentication failed. {}", - CLIENT_ASSERTION_CREDENTIAL, error_response.error_description - ) - }; - Err(Error::with_message(ErrorKind::Credential, message)) - } - } + crate::handle_entra_response(res) } } @@ -181,6 +152,7 @@ impl TokenCredential for ClientAssertionCredential { self.cache .get_token(scopes, options, |s, o| self.get_token_impl(s, o)) .await + .map_err(crate::authentication_error::>) } } @@ -191,7 +163,7 @@ pub(crate) mod tests { use azure_core::{ http::{ headers::{self, content_type, Headers}, - AsyncRawResponse, Body, Method, Request, Transport, + AsyncRawResponse, Body, Method, RawResponse, Request, StatusCode, Transport, }, Bytes, }; @@ -261,16 +233,18 @@ pub(crate) mod tests { #[tokio::test] async fn get_token_error() { - let expected = "error description from the response"; + let body = Bytes::from( + r#"{"error":"invalid_request","error_description":"error description from the response","error_codes":[50027],"timestamp":"2025-04-18 16:04:37Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=50027"}"#, + ); + let mut headers = Headers::default(); + headers.insert("key", "value"); + let expected_status = StatusCode::BadRequest; + let expected_response = + RawResponse::from_bytes(expected_status, headers.clone(), body.clone()); + let mock_response = AsyncRawResponse::from_bytes(expected_status, headers, body); + let mock = MockSts::new( - vec![AsyncRawResponse::from_bytes( - StatusCode::BadRequest, - Headers::default(), - Bytes::from(format!( - r#"{{"error":"invalid_request","error_description":"{}","error_codes":[50027],"timestamp":"2025-04-18 16:04:37Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=50027"}}"#, - expected - )), - )], + vec![mock_response], Some(Arc::new(is_valid_request( FAKE_PUBLIC_CLOUD_AUTHORITY.to_string(), Some(FAKE_ASSERTION.to_string()), @@ -290,16 +264,31 @@ pub(crate) mod tests { ) .expect("valid credential"); - let error = credential + let err = credential .get_token(LIVE_TEST_SCOPES, None) .await .expect_err("authentication error"); - assert!(matches!(error.kind(), ErrorKind::Credential)); - assert!( - error.to_string().contains(expected), - "expected error description from the response, got '{}'", - error + assert!(matches!(err.kind(), ErrorKind::Credential)); + assert_eq!( + "ClientAssertionCredential authentication failed. error description from the response", + err.to_string(), ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: Some(error_code), + raw_response: Some(response), + status, + } => { + assert_eq!("50027", error_code); + assert_eq!(&expected_response, response.as_ref()); + assert_eq!(expected_status, *status); + } + err => panic!("unexpected {:?}", err), + }; } #[tokio::test] diff --git a/sdk/identity/azure_identity/src/client_certificate_credential.rs b/sdk/identity/azure_identity/src/client_certificate_credential.rs index 992b9fe2c5..e4d4781a19 100644 --- a/sdk/identity/azure_identity/src/client_certificate_credential.rs +++ b/sdk/identity/azure_identity/src/client_certificate_credential.rs @@ -2,8 +2,8 @@ // Licensed under the MIT License. use crate::{ - authentication_error, deserialize, env::Env, get_authority_host, validate_not_empty, - validate_tenant_id, EntraIdErrorResponse, EntraIdTokenResponse, TokenCache, + authentication_error, env::Env, get_authority_host, validate_not_empty, validate_tenant_id, + TokenCache, }; use azure_core::{ base64, @@ -12,9 +12,9 @@ use azure_core::{ http::{ headers::{self, content_type}, request::Request, - ClientOptions, Method, Pipeline, PipelineSendOptions, StatusCode, Url, + ClientOptions, Method, Pipeline, PipelineSendOptions, Url, }, - time::{Duration, OffsetDateTime}, + time::OffsetDateTime, Uuid, }; @@ -207,26 +207,7 @@ impl ClientCertificateCredential { ) .await?; - match rsp.status() { - StatusCode::Ok => { - let response: EntraIdTokenResponse = - deserialize(stringify!(ClientCertificateCredential), rsp)?; - Ok(AccessToken::new( - response.access_token, - OffsetDateTime::now_utc() + Duration::seconds(response.expires_in), - )) - } - _ => { - let error_response: EntraIdErrorResponse = - deserialize(stringify!(ClientCertificateCredential), rsp)?; - let message = if error_response.error_description.is_empty() { - "authentication failed".to_string() - } else { - error_response.error_description.clone() - }; - Err(Error::with_message(ErrorKind::Credential, message)) - } - } + crate::handle_entra_response(rsp) } } @@ -306,14 +287,12 @@ impl TokenCredential for ClientCertificateCredential { #[cfg(test)] mod tests { use super::*; - use crate::{ - client_assertion_credential::tests::is_valid_request, tests::*, TSG_LINK_ERROR_TEXT, - }; + use crate::{client_assertion_credential::tests::is_valid_request, tests::*}; use azure_core::{ http::{ headers::Headers, policies::{Policy, PolicyResult}, - AsyncRawResponse, Context, StatusCode, Transport, + AsyncRawResponse, Context, RawResponse, StatusCode, Transport, }, Bytes, }; @@ -473,15 +452,15 @@ mod tests { #[tokio::test] async fn get_token_error() { - let description = "AADSTS7000215: Invalid client certificate."; + let body = Bytes::from( + r#"{"error":"invalid_client","error_description":"AADSTS7000215: Invalid client certificate.","error_codes":[7000215],"timestamp":"2025-04-04 21:10:04Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=7000215"}"#, + ); + let expected_status = StatusCode::BadRequest; + let headers = Headers::default(); + let expected_response = + RawResponse::from_bytes(expected_status, headers.clone(), body.clone()); let sts = MockSts::new( - vec![AsyncRawResponse::from_bytes( - StatusCode::BadRequest, - Headers::default(), - Bytes::from(format!( - r#"{{"error":"invalid_client","error_description":"{description}","error_codes":[7000215],"timestamp":"2025-04-04 21:10:04Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=7000215"}}"#, - )), - )], + vec![AsyncRawResponse::from_bytes(expected_status, headers, body)], Some(Arc::new(is_valid_request( FAKE_PUBLIC_CLOUD_AUTHORITY.to_string(), None, @@ -506,16 +485,26 @@ mod tests { .await .expect_err("expected error"); assert!(matches!(err.kind(), ErrorKind::Credential)); - assert!( - err.to_string().contains(description), - "expected error description from the response, got '{}'", - err - ); - assert!( - err.to_string() - .contains(&format!("{TSG_LINK_ERROR_TEXT}#client-cert")), - "expected error to contain a link to the troubleshooting guide, got '{err}'", + assert_eq!( + "ClientCertificateCredential authentication failed. AADSTS7000215: Invalid client certificate.\nTo troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#client-cert", + err.to_string(), ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: Some(error_code), + raw_response: Some(response), + status, + } => { + assert_eq!("7000215", error_code); + assert_eq!(&expected_response, response.as_ref()); + assert_eq!(expected_status, *status); + } + err => panic!("unexpected {:?}", err), + }; } #[tokio::test] diff --git a/sdk/identity/azure_identity/src/client_secret_credential.rs b/sdk/identity/azure_identity/src/client_secret_credential.rs index eb3ab5e881..83ee041156 100644 --- a/sdk/identity/azure_identity/src/client_secret_credential.rs +++ b/sdk/identity/azure_identity/src/client_secret_credential.rs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::{ - authentication_error, deserialize, get_authority_host, EntraIdErrorResponse, - EntraIdTokenResponse, TokenCache, -}; +use crate::{authentication_error, get_authority_host, TokenCache}; use azure_core::credentials::TokenRequestOptions; -use azure_core::http::{PipelineSendOptions, StatusCode}; +use azure_core::http::PipelineSendOptions; use azure_core::Result; use azure_core::{ credentials::{AccessToken, Secret, TokenCredential}, @@ -15,14 +12,11 @@ use azure_core::{ headers::{self, content_type}, ClientOptions, Method, Pipeline, Request, Url, }, - time::{Duration, OffsetDateTime}, Error, }; use std::{str, sync::Arc}; use url::form_urlencoded; -const CLIENT_SECRET_CREDENTIAL: &str = "ClientSecretCredential"; - /// Options for constructing a new [`ClientSecretCredential`]. #[derive(Debug, Default)] pub struct ClientSecretCredentialOptions { @@ -117,29 +111,7 @@ impl ClientSecretCredential { ) .await?; - match res.status() { - StatusCode::Ok => { - let token_response: EntraIdTokenResponse = - deserialize(CLIENT_SECRET_CREDENTIAL, res)?; - Ok(AccessToken::new( - token_response.access_token, - OffsetDateTime::now_utc() + Duration::seconds(token_response.expires_in), - )) - } - _ => { - let error_response: EntraIdErrorResponse = - deserialize(CLIENT_SECRET_CREDENTIAL, res)?; - let message = if error_response.error_description.is_empty() { - format!("{} authentication failed.", CLIENT_SECRET_CREDENTIAL) - } else { - format!( - "{} authentication failed. {}", - CLIENT_SECRET_CREDENTIAL, error_response.error_description - ) - }; - Err(Error::with_message(ErrorKind::Credential, message)) - } - } + crate::handle_entra_response(res) } } @@ -167,12 +139,13 @@ impl TokenCredential for ClientSecretCredential { #[cfg(test)] mod tests { use super::*; - use crate::{tests::*, TSG_LINK_ERROR_TEXT}; + use crate::tests::*; use azure_core::{ - http::{headers::Headers, AsyncRawResponse, StatusCode, Transport}, + http::{headers::Headers, AsyncRawResponse, RawResponse, StatusCode, Transport}, Bytes, Result, }; use std::vec; + use time::OffsetDateTime; const FAKE_SECRET: &str = "fake secret"; @@ -219,16 +192,16 @@ mod tests { #[tokio::test] async fn get_token_error() { - let description = "AADSTS7000215: Invalid client secret."; + let body = Bytes::from( + r#"{"error":"invalid_client","error_description":"AADSTS7000215: Invalid client secret.","error_codes":[7000215],"timestamp":"2025-04-04 21:10:04Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=7000215"}"#, + ); + let expected_status = StatusCode::BadRequest; + let mut headers = Headers::default(); + headers.insert("key", "value"); + let expected_response = + RawResponse::from_bytes(expected_status, headers.clone(), body.clone()); let sts = MockSts::new( - vec![AsyncRawResponse::from_bytes( - StatusCode::BadRequest, - Headers::default(), - Bytes::from(format!( - r#"{{"error":"invalid_client","error_description":"{}","error_codes":[7000215],"timestamp":"2025-04-04 21:10:04Z","trace_id":"...","correlation_id":"...","error_uri":"https://login.microsoftonline.com/error?code=7000215"}}"#, - description - )), - )], + vec![AsyncRawResponse::from_bytes(expected_status, headers, body)], Some(Arc::new(is_valid_request( FAKE_PUBLIC_CLOUD_AUTHORITY.to_string(), ))), @@ -251,16 +224,26 @@ mod tests { .await .expect_err("expected error"); assert!(matches!(err.kind(), ErrorKind::Credential)); - assert!( - err.to_string().contains(description), - "expected error description from the response, got '{}'", - err - ); - assert!( - err.to_string() - .contains(&format!("{TSG_LINK_ERROR_TEXT}#client-secret")), - "expected error to contain a link to the troubleshooting guide, got '{err}'", + assert_eq!( + "ClientSecretCredential authentication failed. AADSTS7000215: Invalid client secret.\nTo troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#client-secret", + err.to_string(), ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: Some(error_code), + raw_response: Some(response), + status, + } => { + assert_eq!("7000215", error_code); + assert_eq!(&expected_response, response.as_ref()); + assert_eq!(expected_status, *status); + } + err => panic!("unexpected {:?}", err), + }; } #[tokio::test] diff --git a/sdk/identity/azure_identity/src/imds_managed_identity_credential.rs b/sdk/identity/azure_identity/src/imds_managed_identity_credential.rs index dcf38fe2c7..7201ff07aa 100644 --- a/sdk/identity/azure_identity/src/imds_managed_identity_credential.rs +++ b/sdk/identity/azure_identity/src/imds_managed_identity_credential.rs @@ -136,28 +136,28 @@ impl ImdsManagedIdentityCredential { ) .await?; - if !rsp.status().is_success() { - match rsp.status() { + let status = rsp.status(); + if !status.is_success() { + let message = match status { StatusCode::BadRequest => { - return Err(Error::with_message( - ErrorKind::Credential, - "the requested identity has not been assigned to this resource", - )) + "The requested identity has not been assigned to this resource".to_string() } StatusCode::BadGateway | StatusCode::GatewayTimeout => { - return Err(Error::with_message( - ErrorKind::Credential, - "the request failed due to a gateway error", - )) + "The request failed due to a gateway error".to_string() } _ => { let body = String::from_utf8_lossy(rsp.body()); - return Err(Error::with_message( - ErrorKind::Credential, - format!("the request failed: {body}"), - )); + format!("The request failed: {body}") } - } + }; + return Err(Error::new( + ErrorKind::HttpResponse { + error_code: None, + raw_response: Some(Box::new(rsp)), + status, + }, + message, + )); } let token_response: MsiTokenResponse = from_json(rsp.into_body())?; diff --git a/sdk/identity/azure_identity/src/lib.rs b/sdk/identity/azure_identity/src/lib.rs index 5acfc2522c..641083dc8c 100644 --- a/sdk/identity/azure_identity/src/lib.rs +++ b/sdk/identity/azure_identity/src/lib.rs @@ -50,8 +50,10 @@ pub(crate) use virtual_machine_managed_identity_credential::*; use crate::env::Env; use azure_core::{ cloud::CloudConfiguration, - error::{ErrorKind, ResultExt}, + credentials::AccessToken, + error::ErrorKind, http::{RawResponse, Url}, + time::{Duration, OffsetDateTime}, Error, Result, }; use serde::Deserialize; @@ -60,6 +62,7 @@ use std::borrow::Cow; #[derive(Debug, Default, Deserialize)] #[serde(default)] struct EntraIdErrorResponse { + error_codes: Vec, error_description: String, } @@ -74,20 +77,44 @@ struct EntraIdTokenResponse { access_token: String, } -fn deserialize(credential_name: &str, res: RawResponse) -> Result +fn deserialize(res: &RawResponse) -> Result where T: serde::de::DeserializeOwned, { - let t: T = res - .into_body() - .json() - .with_context_fn(ErrorKind::Credential, || { - format!( - "{} authentication failed: invalid response", - credential_name - ) - })?; - Ok(t) + res.body().json() +} + +fn handle_entra_response(response: RawResponse) -> Result { + let status = response.status(); + if status.is_success() { + let token_response: EntraIdTokenResponse = deserialize(&response)?; + return Ok(AccessToken::new( + token_response.access_token, + OffsetDateTime::now_utc() + Duration::seconds(token_response.expires_in), + )); + } + + let error_response: EntraIdErrorResponse = deserialize(&response)?; + let error_code = if error_response.error_codes.is_empty() { + None + } else { + Some( + error_response + .error_codes + .iter() + .map(i32::to_string) + .collect::>() + .join(","), + ) + }; + Err(Error::new( + ErrorKind::HttpResponse { + status, + error_code, + raw_response: Some(Box::new(response)), + }, + error_response.error_description, + )) } fn validate_not_empty(value: &str, message: C) -> Result<()> @@ -134,30 +161,48 @@ fn get_authority_host(env: Option, cloud: Option<&CloudConfiguration>) -> R } const TSG_LINK_ERROR_TEXT: &str = - ". To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot"; + "\nTo troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot"; /// Map an error from a credential's get_token() method to an ErrorKind::Credential error, appending /// a link to the troubleshooting guide entry for that credential, if it has one. -/// -/// TODO: decide whether to map to ErrorKind::Credential here (https://github.com/Azure/azure-sdk-for-rust/issues/3127) -fn authentication_error(e: azure_core::Error) -> azure_core::Error { - azure_core::Error::with_message_fn(e.kind().clone(), || { - let type_name = std::any::type_name::(); - let short_name = type_name.rsplit("::").next().unwrap_or(type_name); // cspell:ignore rsplit - let link = match short_name { - "AzureCliCredential" => format!("{TSG_LINK_ERROR_TEXT}#azure-cli"), - "AzureDeveloperCliCredential" => format!("{TSG_LINK_ERROR_TEXT}#azd"), - "AzurePipelinesCredential" => format!("{TSG_LINK_ERROR_TEXT}#apc"), - #[cfg(feature = "client_certificate")] - "ClientCertificateCredential" => format!("{TSG_LINK_ERROR_TEXT}#client-cert"), - "ClientSecretCredential" => format!("{TSG_LINK_ERROR_TEXT}#client-secret"), - "ManagedIdentityCredential" => format!("{TSG_LINK_ERROR_TEXT}#managed-id"), - "WorkloadIdentityCredential" => format!("{TSG_LINK_ERROR_TEXT}#workload"), - _ => "".to_string(), - }; - - format!("{short_name} authentication failed: {e}{link}") - }) +fn authentication_error(err: azure_core::Error) -> azure_core::Error { + let type_name = std::any::type_name::(); + // remove generic parameters; we want only the credential name + let type_name = type_name.split('<').next().unwrap_or(type_name); + let short_name = type_name.rsplit("::").next().unwrap_or(type_name); // cspell:ignore rsplit + + let (wrap_inner_error, link_fragment) = match short_name { + stringify!(AzureCliCredential) => (false, "#azure-cli"), + stringify!(AzureDeveloperCliCredential) => (false, "#azd"), + stringify!(AzurePipelinesCredential) => (true, "#apc"), + stringify!(ClientCertificateCredential) => (false, "#client-cert"), + stringify!(ClientSecretCredential) => (false, "#client-secret"), + stringify!(ManagedIdentityCredential) => (false, "#managed-id"), + stringify!(WorkloadIdentityCredential) => (true, "#workload"), + _ => (false, ""), + }; + + let mut details = err.to_string(); + if wrap_inner_error { + // details is e.g. "ClientAssertionCredential authentication failed. ". + // We want only the "" part because we want to return an error like + // "OtherCredential authentication failed. ". + const FAILED_STR: &str = " authentication failed. "; + if let Some(index) = details.find(FAILED_STR) { + details.drain(..index + FAILED_STR.len()); + } + } + let mut message = format!("{short_name} authentication failed. {details}"); + if !link_fragment.is_empty() { + message.push_str(TSG_LINK_ERROR_TEXT); + message.push_str(link_fragment); + } + if wrap_inner_error { + let inner = err.into_inner().unwrap_or_else(|e| Box::new(e)); + azure_core::Error::with_error(ErrorKind::Credential, inner, message) + } else { + azure_core::Error::with_error(ErrorKind::Credential, err, message) + } } #[test] diff --git a/sdk/identity/azure_identity/src/managed_identity_credential.rs b/sdk/identity/azure_identity/src/managed_identity_credential.rs index 49961031a0..ff529d4cf9 100644 --- a/sdk/identity/azure_identity/src/managed_identity_credential.rs +++ b/sdk/identity/azure_identity/src/managed_identity_credential.rs @@ -100,9 +100,9 @@ impl TokenCredential for ManagedIdentityCredential { options: Option>, ) -> azure_core::Result { if scopes.len() != 1 { - return Err(azure_core::Error::with_message_fn( + return Err(azure_core::Error::with_message( azure_core::error::ErrorKind::Credential, - || "ManagedIdentityCredential requires exactly one scope".to_string(), + "ManagedIdentityCredential requires exactly one scope".to_string(), )); } self.credential @@ -168,12 +168,13 @@ mod tests { use crate::{ env::Env, tests::{LIVE_TEST_RESOURCE, LIVE_TEST_SCOPES}, - TSG_LINK_ERROR_TEXT, }; - use azure_core::http::headers::Headers; - use azure_core::http::{AsyncRawResponse, Method, Request, StatusCode, Transport, Url}; + use azure_core::http::{ + AsyncRawResponse, Method, RawResponse, Request, StatusCode, Transport, Url, + }; use azure_core::time::OffsetDateTime; use azure_core::Bytes; + use azure_core::{error::ErrorKind, http::headers::Headers}; use azure_core_test::{http::MockHttpClient, recorded}; use futures::FutureExt; use std::env; @@ -207,6 +208,68 @@ mod tests { Ok(()) } + async fn run_error_response_test(source: ManagedIdentitySource) { + let expected_status = StatusCode::ImATeapot; + let headers = Headers::default(); + let content: &str = "is a teapot"; + let body = Bytes::copy_from_slice(content.as_bytes()); + let expected_response = + RawResponse::from_bytes(expected_status, headers.clone(), body.clone()); + let mock_headers = headers.clone(); + let mock_body = body.clone(); + let mock_client = MockHttpClient::new(move |_| { + let headers = mock_headers.clone(); + let body = mock_body.clone(); + async move { Ok(AsyncRawResponse::from_bytes(expected_status, headers, body)) }.boxed() + }); + let test_env = match source { + ManagedIdentitySource::Imds => Env::from(&[][..]), + ManagedIdentitySource::AppService => Env::from( + &[ + ( + IDENTITY_ENDPOINT, + "http://localhost/metadata/identity/oauth2/token", + ), + (IDENTITY_HEADER, "secret"), + ][..], + ), + other => panic!("unsupported managed identity source {:?}", other), + }; + let options = ManagedIdentityCredentialOptions { + client_options: ClientOptions { + transport: Some(Transport::new(Arc::new(mock_client))), + ..Default::default() + }, + env: test_env, + ..Default::default() + }; + let credential = ManagedIdentityCredential::new(Some(options)).expect("credential"); + let err = credential + .get_token(LIVE_TEST_SCOPES, None) + .await + .expect_err("expected error"); + assert!(matches!(err.kind(), ErrorKind::Credential)); + assert_eq!( + "ManagedIdentityCredential authentication failed. The request failed: is a teapot\nTo troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#managed-id", + err.to_string(), + ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: None, + raw_response: Some(response), + status, + } => { + assert_eq!(response.as_ref(), &expected_response); + assert_eq!(expected_status, *status); + } + err => panic!("unexpected {:?}", err), + }; + } + async fn run_supported_source_test( env: Env, options: Option, @@ -377,6 +440,11 @@ mod tests { .await; } + #[tokio::test] + async fn app_service_error_response() { + run_error_response_test(ManagedIdentitySource::AppService).await + } + #[tokio::test] async fn app_service_object_id() { run_app_service_test(Some(ManagedIdentityCredentialOptions { @@ -430,44 +498,6 @@ mod tests { ); } - #[tokio::test] - async fn get_token_error() { - let mock_client = MockHttpClient::new(|_| { - async move { - Ok(AsyncRawResponse::from_bytes( - StatusCode::BadRequest, - Headers::default(), - Bytes::new(), - )) - } - .boxed() - }); - let options = ManagedIdentityCredentialOptions { - client_options: ClientOptions { - transport: Some(Transport::new(Arc::new(mock_client))), - ..Default::default() - }, - ..Default::default() - }; - let credential = ManagedIdentityCredential::new(Some(options)).expect("credential"); - let err = credential - .get_token(LIVE_TEST_SCOPES, None) - .await - .expect_err("expected error"); - assert!(matches!( - err.kind(), - azure_core::error::ErrorKind::Credential - )); - assert!(err - .to_string() - .contains("the requested identity has not been assigned to this resource")); - assert!( - err.to_string() - .contains(&format!("{TSG_LINK_ERROR_TEXT}#managed-id")), - "expected error to contain a link to the troubleshooting guide, got '{err}'", - ); - } - async fn run_imds_live_test(id: Option) -> azure_core::Result<()> { if std::env::var("IDENTITY_IMDS_AVAILABLE").is_err() { println!("Skipped: IMDS isn't available"); @@ -537,6 +567,11 @@ mod tests { .await; } + #[tokio::test] + async fn imds_error_response() { + run_error_response_test(ManagedIdentitySource::Imds).await + } + #[tokio::test] async fn imds_object_id() { run_imds_test(Some(ManagedIdentityCredentialOptions { diff --git a/sdk/identity/azure_identity/src/workload_identity_credential.rs b/sdk/identity/azure_identity/src/workload_identity_credential.rs index bca54070ce..928725d7a5 100644 --- a/sdk/identity/azure_identity/src/workload_identity_credential.rs +++ b/sdk/identity/azure_identity/src/workload_identity_credential.rs @@ -191,12 +191,11 @@ mod tests { client_assertion_credential::tests::{is_valid_request, FAKE_ASSERTION}, env::Env, tests::*, - TSG_LINK_ERROR_TEXT, }; use azure_core::{ http::{ - headers::Headers, AsyncRawResponse, ClientOptions, Method, Request, StatusCode, - Transport, Url, + headers::Headers, AsyncRawResponse, ClientOptions, Method, RawResponse, Request, + StatusCode, Transport, Url, }, Bytes, }; @@ -278,15 +277,16 @@ mod tests { #[tokio::test] async fn get_token_error() { let temp_file = TempFile::new(FAKE_ASSERTION); - let description = "invalid assertion"; + let expected_status = StatusCode::Forbidden; + let body = r#"{"error":"invalid_request","error_description":"invalid assertion"}"#; + let mut headers = Headers::default(); + headers.insert("key", "value"); + let expected_response = RawResponse::from_bytes(expected_status, headers.clone(), body); let mock = MockSts::new( vec![AsyncRawResponse::from_bytes( - StatusCode::BadRequest, - Headers::default(), - Bytes::from(format!( - r#"{{"error":"invalid_request","error_description":"{}"}}"#, - description - )), + expected_status, + headers.clone(), + Bytes::from(body), )], Some(Arc::new(is_valid_request( FAKE_PUBLIC_CLOUD_AUTHORITY.to_string(), @@ -316,16 +316,28 @@ mod tests { .get_token(LIVE_TEST_SCOPES, None) .await .expect_err("expected error"); - assert!(matches!( - err.kind(), - azure_core::error::ErrorKind::Credential - )); - assert!(err.to_string().contains(description)); - assert!( - err.to_string() - .contains(&format!("{TSG_LINK_ERROR_TEXT}#workload")), - "expected error to contain a link to the troubleshooting guide, got '{err}'", + + assert!(matches!(err.kind(), ErrorKind::Credential)); + assert_eq!( + "WorkloadIdentityCredential authentication failed. invalid assertion\nTo troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#workload", + err.to_string(), ); + match err + .downcast_ref::() + .expect("returned error should wrap an azure_core::Error") + .kind() + { + ErrorKind::HttpResponse { + error_code: None, + raw_response: Some(response), + status, + .. + } => { + assert_eq!(&expected_response, response.as_ref()); + assert_eq!(expected_status, *status); + } + kind => panic!("unexpected ErrorKind {:?}", kind), + }; } #[test] From 5cb5239755ce13b60df89b22a6e93e369306c3fe Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:04:26 -0800 Subject: [PATCH 2/7] https Updated link in changelog for troubleshooting guide. --- sdk/identity/azure_identity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md index 8bd975672c..67a45bb00d 100644 --- a/sdk/identity/azure_identity/CHANGELOG.md +++ b/sdk/identity/azure_identity/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features Added -- A `get_token()` error motivated by an HTTP response carries that response. See the [troubleshooting guide](http://aka.ms/azsdk/rust/identity/troubleshoot#find-relevant-information-in-errors) for example code showing how to access the response. +- A `get_token()` error motivated 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. ### Breaking Changes From 4d85b3d0e1eb7615dca9789b30e70e2868c14217 Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Sat, 8 Nov 2025 00:45:39 +0000 Subject: [PATCH 3/7] revise TSG content --- sdk/identity/azure_identity/TROUBLESHOOTING.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sdk/identity/azure_identity/TROUBLESHOOTING.md b/sdk/identity/azure_identity/TROUBLESHOOTING.md index 3673923382..45983770b4 100644 --- a/sdk/identity/azure_identity/TROUBLESHOOTING.md +++ b/sdk/identity/azure_identity/TROUBLESHOOTING.md @@ -42,7 +42,7 @@ This error contains several pieces of information: - __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. -Many credential errors also carry the HTTP response that motivated them. The example below demonstrates how to access that response in such a case. +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. ```rust use azure_core::error::ErrorKind; @@ -56,20 +56,18 @@ if let Err(err) = result { if let Some(inner) = err.downcast_ref::() { if let ErrorKind::HttpResponse { raw_response: Some(response), - status, .. } = inner.kind() { let headers = response.headers(); - let body = String::from_utf8_lossy(response.body()); - eprintln!("status: {status}"); - eprintln!("headers: {headers:?}"); - eprintln!("body: {body}"); + for (name, value) in headers.iter() { + println!("{}: {}", name.as_str(), value.as_str()); + } } } } _ => { - // TODO: handle other kinds of error + todo!("handle other kinds of errors") } } } From 15b266f7de3a85d47597394b8c40accbb61b171b Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Sat, 8 Nov 2025 00:46:48 +0000 Subject: [PATCH 4/7] motivated -> caused --- sdk/identity/azure_identity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md index 67a45bb00d..65585ae911 100644 --- a/sdk/identity/azure_identity/CHANGELOG.md +++ b/sdk/identity/azure_identity/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features Added -- A `get_token()` error motivated 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. +- A `get_token()` error caused by an HTTP response carries that response. See the [troubleshooting guide](http://aka.ms/azsdk/rust/identity/troubleshoot#find-relevant-information-in-errors) for example code showing how to access the response. ### Breaking Changes From c1aeaa8d435330c4c508fd36c3e1b6a515ef9fe7 Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:18:55 +0000 Subject: [PATCH 5/7] simplify error handling for ClientAssertionCredential --- .../src/azure_cli_credential.rs | 2 +- .../src/azure_developer_cli_credential.rs | 2 +- .../src/azure_pipelines_credential.rs | 9 ++-- .../src/client_assertion_credential.rs | 11 ++++- .../src/client_certificate_credential.rs | 2 +- .../src/client_secret_credential.rs | 2 +- sdk/identity/azure_identity/src/lib.rs | 45 +++++-------------- .../src/managed_identity_credential.rs | 2 +- .../src/workload_identity_credential.rs | 8 ++-- 9 files changed, 32 insertions(+), 51 deletions(-) diff --git a/sdk/identity/azure_identity/src/azure_cli_credential.rs b/sdk/identity/azure_identity/src/azure_cli_credential.rs index 0c54520398..1d85e8029a 100644 --- a/sdk/identity/azure_identity/src/azure_cli_credential.rs +++ b/sdk/identity/azure_identity/src/azure_cli_credential.rs @@ -161,7 +161,7 @@ impl TokenCredential for AzureCliCredential { shell_exec::(self.executor.clone(), &self.env, &command) .await - .map_err(authentication_error::) + .map_err(|err| authentication_error(stringify!(AzureCliCredential), err)) } } diff --git a/sdk/identity/azure_identity/src/azure_developer_cli_credential.rs b/sdk/identity/azure_identity/src/azure_developer_cli_credential.rs index f3138f0978..75fce5434c 100644 --- a/sdk/identity/azure_identity/src/azure_developer_cli_credential.rs +++ b/sdk/identity/azure_identity/src/azure_developer_cli_credential.rs @@ -131,7 +131,7 @@ impl TokenCredential for AzureDeveloperCliCredential { } shell_exec::(self.executor.clone(), &self.env, &command) .await - .map_err(authentication_error::) + .map_err(|err| authentication_error(stringify!(AzureDeveloperCliCredential), err)) } } diff --git a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs index b673e1091c..f284b8e154 100644 --- a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs +++ b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs @@ -2,8 +2,7 @@ // Licensed under the MIT License. use crate::{ - authentication_error, env::Env, ClientAssertion, ClientAssertionCredential, - ClientAssertionCredentialOptions, + env::Env, ClientAssertion, ClientAssertionCredential, ClientAssertionCredentialOptions, }; use azure_core::{ credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions}, @@ -109,6 +108,7 @@ impl AzurePipelinesCredential { tenant_id, client_id, client, + stringify!(AzurePipelinesCredential), Some(options.credential_options), )?; @@ -124,10 +124,7 @@ impl TokenCredential for AzurePipelinesCredential { scopes: &[&str], options: Option>, ) -> azure_core::Result { - self.0 - .get_token(scopes, options) - .await - .map_err(authentication_error::) + self.0.get_token(scopes, options).await } } diff --git a/sdk/identity/azure_identity/src/client_assertion_credential.rs b/sdk/identity/azure_identity/src/client_assertion_credential.rs index fda72134da..85b045ff2c 100644 --- a/sdk/identity/azure_identity/src/client_assertion_credential.rs +++ b/sdk/identity/azure_identity/src/client_assertion_credential.rs @@ -18,6 +18,7 @@ const ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-be /// Enables authentication of a Microsoft Entra service principal using a signed client assertion. #[derive(Debug)] pub struct ClientAssertionCredential { + name: &'static str, client_id: String, endpoint: Url, assertion: C, @@ -61,7 +62,11 @@ impl ClientAssertionCredential { options: Option, ) -> azure_core::Result> { Ok(Arc::new(Self::new_exclusive( - tenant_id, client_id, assertion, options, + tenant_id, + client_id, + assertion, + stringify!(ClientAssertionCredential), + options, )?)) } @@ -72,6 +77,7 @@ impl ClientAssertionCredential { tenant_id: String, client_id: String, assertion: C, + name: &'static str, options: Option, ) -> azure_core::Result { validate_tenant_id(&tenant_id)?; @@ -92,6 +98,7 @@ impl ClientAssertionCredential { None, ); Ok(Self { + name, client_id, assertion, endpoint, @@ -152,7 +159,7 @@ impl TokenCredential for ClientAssertionCredential { self.cache .get_token(scopes, options, |s, o| self.get_token_impl(s, o)) .await - .map_err(crate::authentication_error::>) + .map_err(|err| crate::authentication_error(self.name, err)) } } diff --git a/sdk/identity/azure_identity/src/client_certificate_credential.rs b/sdk/identity/azure_identity/src/client_certificate_credential.rs index e4d4781a19..6d4869047b 100644 --- a/sdk/identity/azure_identity/src/client_certificate_credential.rs +++ b/sdk/identity/azure_identity/src/client_certificate_credential.rs @@ -280,7 +280,7 @@ impl TokenCredential for ClientCertificateCredential { self.cache .get_token(scopes, options, |s, o| self.get_token_impl(s, o)) .await - .map_err(authentication_error::) + .map_err(|err| authentication_error(stringify!(ClientCertificateCredential), err)) } } diff --git a/sdk/identity/azure_identity/src/client_secret_credential.rs b/sdk/identity/azure_identity/src/client_secret_credential.rs index 83ee041156..f08eab718e 100644 --- a/sdk/identity/azure_identity/src/client_secret_credential.rs +++ b/sdk/identity/azure_identity/src/client_secret_credential.rs @@ -132,7 +132,7 @@ impl TokenCredential for ClientSecretCredential { self.cache .get_token(scopes, options, |s, o| self.get_token_impl(s, o)) .await - .map_err(authentication_error::) + .map_err(|err| authentication_error(stringify!(ClientSecretCredential), err)) } } diff --git a/sdk/identity/azure_identity/src/lib.rs b/sdk/identity/azure_identity/src/lib.rs index 641083dc8c..787a79dad9 100644 --- a/sdk/identity/azure_identity/src/lib.rs +++ b/sdk/identity/azure_identity/src/lib.rs @@ -165,44 +165,23 @@ const TSG_LINK_ERROR_TEXT: &str = /// Map an error from a credential's get_token() method to an ErrorKind::Credential error, appending /// a link to the troubleshooting guide entry for that credential, if it has one. -fn authentication_error(err: azure_core::Error) -> azure_core::Error { - let type_name = std::any::type_name::(); - // remove generic parameters; we want only the credential name - let type_name = type_name.split('<').next().unwrap_or(type_name); - let short_name = type_name.rsplit("::").next().unwrap_or(type_name); // cspell:ignore rsplit - - let (wrap_inner_error, link_fragment) = match short_name { - stringify!(AzureCliCredential) => (false, "#azure-cli"), - stringify!(AzureDeveloperCliCredential) => (false, "#azd"), - stringify!(AzurePipelinesCredential) => (true, "#apc"), - stringify!(ClientCertificateCredential) => (false, "#client-cert"), - stringify!(ClientSecretCredential) => (false, "#client-secret"), - stringify!(ManagedIdentityCredential) => (false, "#managed-id"), - stringify!(WorkloadIdentityCredential) => (true, "#workload"), - _ => (false, ""), +fn authentication_error(credential_name: &str, err: Error) -> Error { + let link_fragment = match credential_name { + stringify!(AzureCliCredential) => "#azure-cli", + stringify!(AzureDeveloperCliCredential) => "#azd", + stringify!(AzurePipelinesCredential) => "#apc", + stringify!(ClientCertificateCredential) => "#client-cert", + stringify!(ClientSecretCredential) => "#client-secret", + stringify!(ManagedIdentityCredential) => "#managed-id", + stringify!(WorkloadIdentityCredential) => "#workload", + _ => "", }; - - let mut details = err.to_string(); - if wrap_inner_error { - // details is e.g. "ClientAssertionCredential authentication failed. ". - // We want only the "" part because we want to return an error like - // "OtherCredential authentication failed. ". - const FAILED_STR: &str = " authentication failed. "; - if let Some(index) = details.find(FAILED_STR) { - details.drain(..index + FAILED_STR.len()); - } - } - let mut message = format!("{short_name} authentication failed. {details}"); + let mut message = format!("{credential_name} authentication failed. {err}"); if !link_fragment.is_empty() { message.push_str(TSG_LINK_ERROR_TEXT); message.push_str(link_fragment); } - if wrap_inner_error { - let inner = err.into_inner().unwrap_or_else(|e| Box::new(e)); - azure_core::Error::with_error(ErrorKind::Credential, inner, message) - } else { - azure_core::Error::with_error(ErrorKind::Credential, err, message) - } + Error::with_error(ErrorKind::Credential, err, message) } #[test] diff --git a/sdk/identity/azure_identity/src/managed_identity_credential.rs b/sdk/identity/azure_identity/src/managed_identity_credential.rs index ff529d4cf9..b6ee72f125 100644 --- a/sdk/identity/azure_identity/src/managed_identity_credential.rs +++ b/sdk/identity/azure_identity/src/managed_identity_credential.rs @@ -108,7 +108,7 @@ impl TokenCredential for ManagedIdentityCredential { self.credential .get_token(scopes, options) .await - .map_err(authentication_error::) + .map_err(|err| authentication_error(stringify!(ManagedIdentityCredential), err)) } } diff --git a/sdk/identity/azure_identity/src/workload_identity_credential.rs b/sdk/identity/azure_identity/src/workload_identity_credential.rs index 928725d7a5..4dfa316fac 100644 --- a/sdk/identity/azure_identity/src/workload_identity_credential.rs +++ b/sdk/identity/azure_identity/src/workload_identity_credential.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::{authentication_error, env::Env}; +use crate::env::Env; use async_lock::{RwLock, RwLockUpgradableReadGuard}; use azure_core::{ credentials::{AccessToken, Secret, TokenCredential, TokenRequestOptions}, @@ -86,6 +86,7 @@ impl WorkloadIdentityCredential { tenant_id, client_id, Token::new(path)?, + stringify!(WorkloadIdentityCredential), Some(options.credential_options), )?, ))) @@ -106,10 +107,7 @@ impl TokenCredential for WorkloadIdentityCredential { "no scopes specified", )); } - self.0 - .get_token(scopes, options) - .await - .map_err(authentication_error::) + self.0.get_token(scopes, options).await } } From 7a1b8a3785e6e05c8af5bfff68ba49e60a8a78cb Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:20:37 +0000 Subject: [PATCH 6/7] remove unnecessary variable --- .../azure_identity/src/azure_pipelines_credential.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs index f284b8e154..5bc6d0f1d5 100644 --- a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs +++ b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs @@ -163,16 +163,15 @@ impl ClientAssertion for Client { let status = resp.status(); if status != StatusCode::Ok { let err_headers: ErrorHeaders = resp.headers().get()?; - let message = format!( - "{status} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}" - ); return Err(azure_core::Error::with_message( ErrorKind::HttpResponse { status, error_code: Some(status.canonical_reason().to_string()), raw_response: Some(Box::new(resp)), }, - message, + format!( + "{status} response from the OIDC endpoint. Check service connection ID and pipeline configuration. {err_headers}" + ), )); } From 1b19e89618e262f94eb4d05c8a3997e1d1a9e490 Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:37:28 -0800 Subject: [PATCH 7/7] fix TSG link --- sdk/identity/azure_identity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md index 65585ae911..578fae8be9 100644 --- a/sdk/identity/azure_identity/CHANGELOG.md +++ b/sdk/identity/azure_identity/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features Added -- A `get_token()` error caused by an HTTP response carries that response. See the [troubleshooting guide](http://aka.ms/azsdk/rust/identity/troubleshoot#find-relevant-information-in-errors) for example code showing how to access the response. +- 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. ### Breaking Changes