diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md
index 2469ba3395c9..7fc6c810a83a 100644
--- a/sdk/identity/azure-identity/CHANGELOG.md
+++ b/sdk/identity/azure-identity/CHANGELOG.md
@@ -8,6 +8,8 @@
### Bugs Fixed
+- Fixed `AzureDeveloperCliCredential` error parsing for Azure Developer CLI v1.23.7 and later, which previously surfaced the friendly wrapper "Authentication with Azure failed." instead of the underlying error text. The parser now prefers the structured top-level `error` field while preserving fallback behavior for older `consoleMessage` output.
+- Structured AAD failures from `azd` (e.g. `invalid_tenant`, `AADSTS*`) now surface as `ClientAuthenticationException` rather than being misclassified as `CredentialUnavailableException`.
- Disabled MSAL's internal retry for Confidential Client, Managed Identity and Public Client Applications.
### Other Changes
diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java
index 4d584d15b4f5..bf21f27a1fb3 100644
--- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java
+++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java
@@ -744,16 +744,19 @@ AccessToken getTokenFromAzureDeveloperCLIAuthentication(StringBuilder azdCommand
+ "to support claims challenges."));
}
- if (redactedOutput.contains("azd auth login") || redactedOutput.contains("not logged in")) {
+ String parsedMessage = getAzdErrorMessage(redactedOutput);
+
+ // Dispatch on the parsed message: azd v1.23.7+ embeds "azd auth login" in the
+ // "suggestion" field of every structured failure, so checking 'redactedOutput' would
+ // misroute real AAD errors to CredentialUnavailableException.
+ if (parsedMessage.contains("azd auth login") || parsedMessage.contains("not logged in")) {
if (azdCommand.toString().contains("claims")) {
- throw LOGGER.logExceptionAsError(
- new ClientAuthenticationException(getAzdErrorMessage(redactedOutput), null));
+ throw LOGGER.logExceptionAsError(new ClientAuthenticationException(parsedMessage, null));
}
throw LoggingUtil.logCredentialUnavailableException(LOGGER, options,
- new CredentialUnavailableException(getAzdErrorMessage(redactedOutput)));
+ new CredentialUnavailableException(parsedMessage));
}
- throw LOGGER.logExceptionAsError(
- new ClientAuthenticationException(getAzdErrorMessage(redactedOutput), null));
+ throw LOGGER.logExceptionAsError(new ClientAuthenticationException(parsedMessage, null));
} else {
throw LOGGER.logExceptionAsError(
new ClientAuthenticationException("Failed to invoke Azure Developer CLI ", null));
@@ -787,23 +790,32 @@ AccessToken getTokenFromAzureDeveloperCLIAuthentication(StringBuilder azdCommand
}
/**
- * Extract a single, user-friendly message from azd consoleMessage JSON output.
+ * Extract a single, user-friendly message from azd's stderr JSON output.
+ *
+ *
azd writes JSON error messages to stderr. The format depends on the azd version:
+ *
+ * - v1.24.0+: {@code {"error":"...","message":"...","suggestion":"..."}} (single line)
+ * - v1.23.7 - v1.23.15: an empty {@code {"type":"consoleMessage",...}} line followed by
+ * the structured {@code {"error":"..."}} line
+ * - pre-v1.23.7 (legacy): a single {@code {"type":"consoleMessage","data":{"message":"..."}}}
+ * line whose {@code data.message} carries the entire {@code ERROR: ...} output
+ *
+ *
+ * The structured {@code "error"} field is preferred when present; otherwise the function
+ * falls back to the first non-empty legacy {@code consoleMessage} {@code data.message}.
+ * The top-level {@code "message"} field is intentionally ignored: in newer azd it carries the
+ * friendly wrapper "Authentication with Azure failed." which we don't want to surface in place
+ * of the actual error.
*
* @param output The output from the Azure Developer CLI command.
* @return A user-friendly error message if found, otherwise null.
- *
- * Preference order:
- * 1) A message containing "Suggestion" (case-insensitive)
- * 2) The second message if multiple are present
- * 3) The first message if only one exists
- * Returns null if no messages can be parsed.
*/
String extractUserFriendlyErrorFromAzdOutput(String output) {
if (output == null || output.isEmpty()) {
return null;
}
- List messages = new ArrayList<>();
+ String fallback = null;
for (String line : output.split("\\R")) { // split on any line break
String trimmed = line.trim();
@@ -811,13 +823,27 @@ String extractUserFriendlyErrorFromAzdOutput(String output) {
continue;
}
- // Handle multiple JSON objects in a single line
+ // Defensive: the read loop above uses readLine() (which strips line terminators)
+ // and appends without re-inserting newlines, so multiple JSON objects can end up
+ // glued together on the same logical line.
try (JsonReader reader = JsonProviders.createReader(trimmed)) {
while (reader.nextToken() != null) {
- if (reader.currentToken() == JsonToken.START_OBJECT) {
- Map obj = reader.readMap(JsonReader::readUntyped);
+ if (reader.currentToken() != JsonToken.START_OBJECT) {
+ break;
+ }
+ Map obj = reader.readMap(JsonReader::readUntyped);
+
+ // Prefer structured top-level "error" (azd v1.23.7+).
+ Object errorField = obj.get("error");
+ if (errorField instanceof String) {
+ String errorMsg = ((String) errorField).trim();
+ if (!errorMsg.isEmpty()) {
+ return redactInfo(errorMsg);
+ }
+ }
- // check "data.message"
+ // Otherwise fall back to first non-empty legacy data.message.
+ if (fallback == null) {
Object data = obj.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
@@ -826,22 +852,10 @@ String extractUserFriendlyErrorFromAzdOutput(String output) {
if (message instanceof String) {
String msg = ((String) message).trim();
if (!msg.isEmpty()) {
- messages.add(msg);
- continue;
+ fallback = msg;
}
}
}
-
- // check "message"
- Object message = obj.get("message");
- if (message instanceof String) {
- String msg = ((String) message).trim();
- if (!msg.isEmpty()) {
- messages.add(msg);
- }
- }
- } else {
- break; // Not a JSON object, stop processing this line
}
}
} catch (IOException e) {
@@ -849,23 +863,7 @@ String extractUserFriendlyErrorFromAzdOutput(String output) {
}
}
- if (messages.isEmpty()) {
- return null;
- }
-
- // Prefer the suggestion line if present
- for (String msg : messages) {
- if (msg.toLowerCase().contains("suggestion")) {
- return redactInfo(msg);
- }
- }
-
- // If more than one message exists, return the last one
- if (messages.size() > 1) {
- return redactInfo(messages.get(messages.size() - 1));
- }
-
- return redactInfo(messages.get(0));
+ return fallback != null ? redactInfo(fallback) : null;
}
// Gets a user-friendly error message from azd output, with fallback to the raw output
diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java
index cab6ec299980..7edf725803dc 100644
--- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java
+++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java
@@ -692,38 +692,41 @@ private void mockForUserRefreshTokenFlow(String token, TokenRequestContext reque
}
@Test
- public void testExtractSuggestionMessagePreferred() {
- // Should prefer messages containing 'Suggestion' (case-insensitive)
+ public void testExtractFirstNonEmptyDataMessage() {
+ // Real-world pre-v1.23.7 azd auth token output: the full ERROR/Suggestion blob is emitted
+ // inside a single consoleMessage data.message. The first non-empty data.message wins.
String output
= "{\"type\":\"consoleMessage\",\"timestamp\":\"2025-08-18T15:08:14.4849845-07:00\",\"data\":{\"message\":\"\\nERROR: fetching token: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access 'tenant-id'. Trace ID: trace-id Correlation ID: correlation-id Timestamp: 2025-08-18 22:08:14Z\\n\"}}\n"
+ "{\"type\":\"consoleMessage\",\"timestamp\":\"2025-08-18T15:08:14.4849845-07:00\",\"data\":{\"message\":\"Suggestion: re-authentication required, run `azd auth login` to acquire a new token.\\n\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Suggestion: re-authentication required, run `azd auth login` to acquire a new token.", result);
+ assertEquals(
+ "ERROR: fetching token: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access 'tenant-id'. Trace ID: trace-id Correlation ID: correlation-id Timestamp: 2025-08-18 22:08:14Z",
+ result);
}
@Test
- public void testExtractSuggestionCaseInsensitive() {
- // Should find 'suggestion' in any case
+ public void testFirstMessageWinsOverSuggestionPrefix() {
+ // Suggestion preference was removed; first non-empty wins (matches Go/.NET/JS/Python).
String output = "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"First message\"}}\n"
+ "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"SUGGESTION: Try running azd auth login\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("SUGGESTION: Try running azd auth login", result);
+ assertEquals("First message", result);
}
@Test
- public void testExtractLastMessageWhenNoSuggestion() {
- // Should return last message when multiple messages but no suggestion
+ public void testExtractFirstMessageWhenMultiplePresent() {
+ // Multiple consoleMessage lines: first non-empty wins.
String output = "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"First error message\"}}\n"
+ "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"Second error message\"}}\n"
+ "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"Third error message\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Third error message", result);
+ assertEquals("First error message", result);
}
@Test
@@ -747,24 +750,26 @@ public void testExtractMessageFromNestedData() {
}
@Test
- public void testExtractMessageFromRootLevel() {
- // Should extract message from root level of JSON
+ public void testRootLevelMessageFieldNotExtracted() {
+ // Only the structured "error" field or legacy data.message is extracted.
+ // Root-level "message" alone is the new friendly wrapper ("Authentication with Azure failed.")
+ // that we intentionally do not surface.
String output = "{\"message\":\"Root level error message\"}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Root level error message", result);
+ assertNull(result);
}
@Test
- public void testExtractMixedMessageLocations() {
- // Should handle messages at different JSON levels
+ public void testRootLevelMessageNotExtractedWhenMixedWithData() {
+ // Root-level "message" is ignored; first non-empty data.message wins.
String output = "{\"message\":\"Root level message\"}\n" + "{\"data\":{\"message\":\"Nested message\"}}\n"
+ "{\"data\":{\"message\":\"suggestion: Use this suggestion\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("suggestion: Use this suggestion", result);
+ assertEquals("Nested message", result);
}
@Test
@@ -780,13 +785,13 @@ public void testIgnoreEmptyMessages() {
@Test
public void testIgnoreNonJsonLines() {
- // Should ignore lines that are not valid JSON
+ // Should ignore lines that are not valid JSON; first non-empty data.message wins.
String output = "This is not JSON\n" + "{\"data\":{\"message\":\"Valid JSON message\"}}\n"
+ "Another non-JSON line\n" + "{\"data\":{\"message\":\"Suggestion: Another valid message\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Suggestion: Another valid message", result);
+ assertEquals("Valid JSON message", result);
}
@Test
@@ -802,13 +807,13 @@ public void testIgnoreNonStringMessages() {
@Test
public void testIgnoreEmptyLines() {
- // Should ignore empty lines and whitespace-only lines
+ // Empty lines are skipped; first non-empty data.message wins.
String output
= "{\"data\":{\"message\":\"First message\"}}\n" + "\n" + "{\"data\":{\"message\":\"Second message\"}}\n";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Second message", result);
+ assertEquals("First message", result);
}
@Test
@@ -860,8 +865,9 @@ public void testReturnNullForWhitespaceOnlyOutput() {
}
@Test
- public void testComplexRealWorldExample() {
- // Should handle complex real-world azd output
+ public void testComplexRealWorldPreV1237Example() {
+ // Pre-v1.23.7 real-world azd output: a single consoleMessage carrying the entire ERROR blob.
+ // First non-empty data.message wins; later progress events and Suggestion lines are ignored.
String output
= "{\"type\":\"consoleMessage\",\"timestamp\":\"2025-08-18T15:08:14.4849845-07:00\",\"data\":{\"message\":\"\\nERROR: fetching token: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access 'tenant-id'. Trace ID: trace-id Correlation ID: correlation-id Timestamp: 2025-08-18 22:08:14Z\\n\"}}\n"
+ "{\"type\":\"consoleMessage\",\"timestamp\":\"2025-08-18T15:08:14.4849845-07:00\",\"data\":{\"message\":\"Suggestion: re-authentication required, run `azd auth login` to acquire a new token.\\n\"}}\n"
@@ -869,7 +875,9 @@ public void testComplexRealWorldExample() {
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Suggestion: re-authentication required, run `azd auth login` to acquire a new token.", result);
+ assertEquals(
+ "ERROR: fetching token: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access 'tenant-id'. Trace ID: trace-id Correlation ID: correlation-id Timestamp: 2025-08-18 22:08:14Z",
+ result);
}
@Test
@@ -884,37 +892,37 @@ public void testStripWhitespaceFromMessages() {
@Test
public void testHandleMalformedJsonGracefully() {
- // Should handle malformed JSON lines gracefully
+ // Malformed JSON lines should be skipped; first non-empty data.message wins.
String output = "{\"data\":{\"message\":\"First valid message\"}}\n"
+ "{\"malformed\":\"json\"without\"closing\"brace\"\n"
- + "{\"data\":{\"message\":\"suggestion: This should be found\"}}";
+ + "{\"data\":{\"message\":\"suggestion: This is ignored after first non-empty\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("suggestion: This should be found", result);
+ assertEquals("First valid message", result);
}
@Test
- public void testMultipleSuggestionMessages() {
- // Should return the first suggestion message found
+ public void testFirstNonEmptyWinsOverLaterSuggestionLines() {
+ // First non-empty data.message wins regardless of any later "Suggestion" content.
String output = "{\"data\":{\"message\":\"First message\"}}\n"
+ "{\"data\":{\"message\":\"Suggestion: First suggestion\"}}\n"
+ "{\"data\":{\"message\":\"Another suggestion: Second suggestion\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Suggestion: First suggestion", result);
+ assertEquals("First message", result);
}
@Test
- public void testSuggestionWithDifferentCasing() {
- // Should find suggestion with various casing
+ public void testSuggestionCasingIgnored() {
+ // Different casings of "suggestion" no longer get special treatment.
String output = "{\"data\":{\"message\":\"Regular message\"}}\n"
+ "{\"data\":{\"message\":\"sUgGeStIoN: Mixed case suggestion\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("sUgGeStIoN: Mixed case suggestion", result);
+ assertEquals("Regular message", result);
}
@Test
@@ -960,13 +968,13 @@ public void testEmptyDataObject() {
@Test
public void testMixedValidAndInvalidJson() {
- // Should handle mix of valid and invalid JSON gracefully
+ // First non-empty parseable data.message wins regardless of later content.
String output = "{\"data\":{\"message\":\"First valid message\"}}\n" + "not json at all\n"
+ "{\"incomplete\": \"json\n" + "{\"data\":{\"message\":\"Suggestion: Final message\"}}";
IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
String result = client.extractUserFriendlyErrorFromAzdOutput(output);
- assertEquals("Suggestion: Final message", result);
+ assertEquals("First valid message", result);
}
@Test
@@ -996,6 +1004,143 @@ public void testGetAzdErrorMessageFallsBackForWhitespaceOnlyInput() {
assertEquals(output, result);
}
+ // --- Tests for new azd structured error format (v1.23.7+) ---
+
+ @Test
+ public void testExtractStructuredErrorFromSingleLine() {
+ // azd v1.24.0+ writes a single-line structured error to stderr.
+ String aadError
+ = "fetching token: failed to authenticate:\n(invalid_tenant) AADSTS90002: Tenant 'test' not found";
+ String output = "{\"error\":\"" + aadError.replace("\n", "\\n")
+ + "\",\"links\":[{\"title\":\"azd auth login reference\",\"url\":\"https://example.com\"}]"
+ + ",\"message\":\"Authentication with Azure failed.\""
+ + ",\"suggestion\":\"Run 'azd auth login' to sign in again.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(aadError, result);
+ }
+
+ @Test
+ public void testExtractStructuredErrorPrecededByEmptyConsoleMessage() {
+ // azd v1.23.7 - v1.23.15 emits an empty consoleMessage line preceding the structured error.
+ String aadError = "fetching token: failed to authenticate";
+ String output
+ = "{\"type\":\"consoleMessage\",\"timestamp\":\"2026-04-13T17:43:24.7558297-07:00\",\"data\":{\"message\":\"\\n\"}}\n"
+ + "{\"error\":\"" + aadError + "\",\"message\":\"Authentication with Azure failed.\""
+ + ",\"suggestion\":\"Run 'azd auth login' to sign in again.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(aadError, result);
+ }
+
+ @Test
+ public void testStructuredErrorOnSameLineAsConsoleMessage() {
+ // azd v1 path concatenates lines (redirectErrorStream + append without newline),
+ // so the two JSON objects can end up on the same physical line.
+ String aadError = "AADSTS90002: Tenant 'test' not found";
+ String output = "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"\\n\"}}" + "{\"error\":\"" + aadError
+ + "\",\"message\":\"Authentication with Azure failed.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(aadError, result);
+ }
+
+ @Test
+ public void testStructuredErrorPreferredOverConsoleMessage() {
+ // The structured "error" line carries the actionable failure and should win over
+ // any consoleMessage data.message (which now just carries the friendly wrapper).
+ String aadError = "AADSTS70008: Refresh token expired";
+ String output = "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"some informational console output\"}}\n"
+ + "{\"error\":\"" + aadError + "\",\"message\":\"Authentication with Azure failed.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(aadError, result);
+ }
+
+ @Test
+ public void testStructuredErrorPreferredOverTopLevelMessage() {
+ // When both "error" and top-level "message" are present on the same object,
+ // prefer the structured "error" field.
+ String aadError = "AADSTS50076: MFA required";
+ String output = "{\"error\":\"" + aadError + "\",\"message\":\"Authentication with Azure failed.\""
+ + ",\"suggestion\":\"Run 'azd auth login' to sign in again.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(aadError, result);
+ }
+
+ @Test
+ public void testEmptyStructuredErrorFallsBackToConsoleMessage() {
+ // An empty/whitespace "error" field should fall through to legacy data.message parsing.
+ String output = "{\"error\":\"\",\"message\":\"Authentication with Azure failed.\"}\n"
+ + "{\"type\":\"consoleMessage\",\"data\":{\"message\":\"ERROR: real message\"}}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals("ERROR: real message", result);
+ }
+
+ @Test
+ public void testNonStringStructuredErrorIgnored() {
+ // A non-string "error" field should be ignored and parsing should continue.
+ String output = "{\"error\":123,\"message\":\"Authentication with Azure failed.\"}\n"
+ + "{\"data\":{\"message\":\"ERROR: real message\"}}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals("ERROR: real message", result);
+ }
+
+ @Test
+ public void testFirstStructuredErrorWinsAcrossLines() {
+ // The first encountered structured error wins.
+ String firstError = "AADSTS90002: Tenant 'test' not found";
+ String output = "{\"error\":\"" + firstError + "\",\"message\":\"Authentication with Azure failed.\"}\n"
+ + "{\"error\":\"Some later error\",\"message\":\"...\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String result = client.extractUserFriendlyErrorFromAzdOutput(output);
+ assertEquals(firstError, result);
+ }
+
+ // --- Dispatch invariant tests ---
+ // getTokenFromAzureDeveloperCLIAuthentication keys exception type off the parsed message
+ // containing "azd auth login" / "not logged in". These pin the parser contract that those
+ // substrings only survive parsing for genuine not-logged-in output.
+
+ @Test
+ public void testParsedStructuredAadErrorDoesNotContainAzdAuthLogin() {
+ // The "azd auth login" text lives in "suggestion" which the parser drops, so structured
+ // AAD errors surface as auth failures rather than credential-unavailable.
+ String output = "{\"error\":\"AADSTS50076: Multi-factor authentication required\""
+ + ",\"message\":\"Authentication with Azure failed.\""
+ + ",\"suggestion\":\"Run 'azd auth login' to sign in again.\"}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String parsed = client.getAzdErrorMessage(output);
+ assertEquals("AADSTS50076: Multi-factor authentication required", parsed);
+ assertFalse(parsed.contains("azd auth login"));
+ assertFalse(parsed.contains("not logged in"));
+ }
+
+ @Test
+ public void testParsedLegacyNotLoggedInRetainsAzdAuthLogin() {
+ // Pre-v1.23.7 "not logged in" output: substring is in data.message and must survive parsing
+ // so dispatch still routes it to CredentialUnavailableException.
+ String output = "{\"type\":\"consoleMessage\",\"data\":{\"message\":"
+ + "\"ERROR: not logged in, run `azd auth login` to login\"}}";
+
+ IdentityClient client = new IdentityClientBuilder().clientId("dummy").build();
+ String parsed = client.getAzdErrorMessage(output);
+ assertTrue(parsed.contains("azd auth login"));
+ assertTrue(parsed.contains("not logged in"));
+ }
+
@Test
public void testManagedCredentialSkipsImdsProbing() {
String accessToken = "token";