From 3a8cec8675664ccc877aa692d763169474f51523 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Tue, 12 May 2026 11:25:32 -0700 Subject: [PATCH 1/7] Add OnboardingBlockingErrorParser bridging x-ms-clitelem to onboarding blob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides a small Kotlin helper that callers (OneAuth navigation fragment, broker error handler, etc.) can invoke after a failed token request to extract the eSTS-emitted error code from MicrosoftTokenResponse (which already carries the parsed x-ms-clitelem header values) and feed it into OnboardingTelemetryRecorder.addBlockingError(...). Per design spec §10, position-2 errorCode of the x-ms-clitelem header is the canonical attribution source for blocking errors. Sub-error code is preferred when present for finer-grained classification; otherwise falls back to the top-level error code. Returns null when no error or error is '0'. Includes 9 unit tests covering both overloads (MicrosoftTokenResponse and raw header string). --- .../OnboardingBlockingErrorParser.kt | 101 +++++++++++++++++ .../OnboardingBlockingErrorParserTest.kt | 104 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt create mode 100644 common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt new file mode 100644 index 0000000000..98cd2db2bc --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.telemetry + +import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResponse + +/** + * Bridges eSTS-emitted error codes (carried via the `x-ms-clitelem` header and parsed + * into [MicrosoftTokenResponse] by [com.microsoft.identity.common.java.providers.microsoft.microsoftsts.AbstractMicrosoftStsTokenResponseHandler]) + * into the onboarding telemetry blob's `lastBlockingError` / `blockingErrors` fields. + * + * Callers (OneAuth navigation fragment, broker error handler, etc.) invoke + * [extractBlockingError] on the [MicrosoftTokenResponse] of a failed token request + * to obtain a string suitable for + * [com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder.addBlockingError]. + * + * Returns null when the response carries no error or the error is not blocking. + * + * Per design spec (Mobile Onboarding Telemetry §10), `errorCode` from position 2 of the + * `x-ms-clitelem` header is the canonical attribution source for blocking errors. + * Sub-error code (position 3) is also captured for finer-grained classification. + */ +object OnboardingBlockingErrorParser { + + /** + * Extract a blocking-error attribution string from a [MicrosoftTokenResponse]. + * + * Returns the most specific available identifier: + * 1. `serverSubErrorCode` (e.g. `interaction_required`) if present and non-zero, else + * 2. `serverErrorCode` (e.g. `65001`) if present and non-zero, else + * 3. `null` (no blocking error to record). + * + * Position-2 of the `x-ms-clitelem` header is `0` when there is no error — those + * cases are filtered out so callers don't pollute the blob with `"0"`. + * + * @return blocking error identifier suitable for `addBlockingError(...)`, or null + */ + @JvmStatic + fun extractBlockingError(tokenResponse: MicrosoftTokenResponse?): String? { + if (tokenResponse == null) return null + + val subError = tokenResponse.cliTelemSubErrorCode + if (!subError.isNullOrBlank() && subError != "0") { + return subError + } + + val error = tokenResponse.cliTelemErrorCode + if (!error.isNullOrBlank() && error != "0") { + return error + } + + return null + } + + /** + * Convenience overload that parses a raw `x-ms-clitelem` header string directly. + * Useful when the caller does not have a [MicrosoftTokenResponse] in hand + * (e.g. parsing a redirect response in a WebView client). + * + * @return blocking error identifier suitable for `addBlockingError(...)`, or null + */ + @JvmStatic + fun extractBlockingError(xMsCliTelemHeader: String?): String? { + if (xMsCliTelemHeader.isNullOrBlank()) return null + + @Suppress("DEPRECATION") + val cliTelemInfo = CliTelemInfo.fromXMsCliTelemHeader(xMsCliTelemHeader) ?: return null + + val subError = cliTelemInfo.serverSubErrorCode + if (!subError.isNullOrBlank() && subError != "0") { + return subError + } + + val error = cliTelemInfo.serverErrorCode + if (!error.isNullOrBlank() && error != "0") { + return error + } + + return null + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt new file mode 100644 index 0000000000..4120f01520 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.telemetry + +import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResponse +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class OnboardingBlockingErrorParserTest { + + // --- MicrosoftTokenResponse overload --- + + @Test + fun nullResponseReturnsNull() { + assertNull(OnboardingBlockingErrorParser.extractBlockingError(null as MicrosoftTokenResponse?)) + } + + @Test + fun emptyResponseReturnsNull() { + val response = MicrosoftTokenResponse() + assertNull(OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun zeroErrorCodeReturnsNull() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("0") + setCliTelemSubErrorCode("0") + } + assertNull(OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun subErrorPreferredOverError() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("65001") + setCliTelemSubErrorCode("interaction_required") + } + assertEquals("interaction_required", OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun errorReturnedWhenNoSubError() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("53000") + } + assertEquals("53000", OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun errorReturnedWhenSubErrorIsZero() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("53000") + setCliTelemSubErrorCode("0") + } + assertEquals("53000", OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + // --- Header string overload --- + + @Test + fun nullHeaderReturnsNull() { + assertNull(OnboardingBlockingErrorParser.extractBlockingError(null as String?)) + } + + @Test + fun blankHeaderReturnsNull() { + assertNull(OnboardingBlockingErrorParser.extractBlockingError("")) + } + + @Test + fun headerWithErrorReturnsErrorCode() { + // Format: version,errorCode,subErrorCode,timeSinceTokenIssuance,tokenRoutingHint + val header = "1,53000,0,1234,routinghint" + assertEquals("53000", OnboardingBlockingErrorParser.extractBlockingError(header)) + } + + @Test + fun headerWithZeroErrorReturnsNull() { + val header = "1,0,0,1234,routinghint" + assertNull(OnboardingBlockingErrorParser.extractBlockingError(header)) + } +} From a01c5aa15a1e3815c2d51ca3120cdad074b11c82 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Wed, 13 May 2026 10:18:52 -0700 Subject: [PATCH 2/7] OnboardingBlockingErrorParser: filter non-onboarding AADSTS error codes Some 5-digit AADSTS codes look like blocking errors syntactically but are not onboarding-remediation signals. Excluding them at the policy boundary so callers don't have to filter individually: - 50058 UserInformationNotProvided (no SSO session - normal sign-in path) - 50097 DeviceAuthenticationRequired (in-flow device auth challenge; if WPJ runs we already record DeviceRegistrationStarted) - 50126 InvalidUserNameOrPassword (wrong credentials - user error) Filter applies in both extractBlockingError overloads (MicrosoftTokenResponse and raw x-ms-clitelem header string). Adds 6 new tests covering each excluded code, sub-error fallthrough behavior, and a sanity check that non-excluded codes still pass. --- .../OnboardingBlockingErrorParser.kt | 33 ++++++++++-- .../OnboardingBlockingErrorParserTest.kt | 53 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt index 98cd2db2bc..6a7a8a368f 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt @@ -42,6 +42,25 @@ import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResp */ object OnboardingBlockingErrorParser { + /** + * AADSTS error codes that look like blocking errors syntactically (5-digit + * server error codes from eSTS) but are NOT onboarding-remediation signals. + * These flow through the parser the same as any other server error, so we + * filter them here at the policy boundary so callers don't have to. + * + * - 50058 UserInformationNotProvided (no SSO session — user just needs to sign in) + * - 50097 DeviceAuthenticationRequired (in-flow device auth challenge; if WPJ runs we + * already record DeviceRegistrationStarted as a step) + * - 50126 InvalidUserNameOrPassword (wrong credentials — user error) + */ + private val NON_ONBOARDING_AADSTS_CODES = setOf("50058", "50097", "50126") + + /** + * Returns true if the candidate error code should be excluded from the + * onboarding blob's `blocking_errors[]`. See [NON_ONBOARDING_AADSTS_CODES]. + */ + private fun isExcluded(candidate: String): Boolean = candidate in NON_ONBOARDING_AADSTS_CODES + /** * Extract a blocking-error attribution string from a [MicrosoftTokenResponse]. * @@ -53,6 +72,9 @@ object OnboardingBlockingErrorParser { * Position-2 of the `x-ms-clitelem` header is `0` when there is no error — those * cases are filtered out so callers don't pollute the blob with `"0"`. * + * Codes in [NON_ONBOARDING_AADSTS_CODES] are also filtered out as they are not + * onboarding-remediation signals. + * * @return blocking error identifier suitable for `addBlockingError(...)`, or null */ @JvmStatic @@ -60,12 +82,12 @@ object OnboardingBlockingErrorParser { if (tokenResponse == null) return null val subError = tokenResponse.cliTelemSubErrorCode - if (!subError.isNullOrBlank() && subError != "0") { + if (!subError.isNullOrBlank() && subError != "0" && !isExcluded(subError)) { return subError } val error = tokenResponse.cliTelemErrorCode - if (!error.isNullOrBlank() && error != "0") { + if (!error.isNullOrBlank() && error != "0" && !isExcluded(error)) { return error } @@ -77,6 +99,9 @@ object OnboardingBlockingErrorParser { * Useful when the caller does not have a [MicrosoftTokenResponse] in hand * (e.g. parsing a redirect response in a WebView client). * + * Codes in [NON_ONBOARDING_AADSTS_CODES] are filtered out the same as in the + * [MicrosoftTokenResponse] overload. + * * @return blocking error identifier suitable for `addBlockingError(...)`, or null */ @JvmStatic @@ -87,12 +112,12 @@ object OnboardingBlockingErrorParser { val cliTelemInfo = CliTelemInfo.fromXMsCliTelemHeader(xMsCliTelemHeader) ?: return null val subError = cliTelemInfo.serverSubErrorCode - if (!subError.isNullOrBlank() && subError != "0") { + if (!subError.isNullOrBlank() && subError != "0" && !isExcluded(subError)) { return subError } val error = cliTelemInfo.serverErrorCode - if (!error.isNullOrBlank() && error != "0") { + if (!error.isNullOrBlank() && error != "0" && !isExcluded(error)) { return error } diff --git a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt index 4120f01520..463ec90dcc 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt @@ -101,4 +101,57 @@ class OnboardingBlockingErrorParserTest { val header = "1,0,0,1234,routinghint" assertNull(OnboardingBlockingErrorParser.extractBlockingError(header)) } + + // --- Non-onboarding AADSTS code whitelist --- + + @Test + fun excludedAadstsCode_50058_FilteredFromResponse() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("50058") // UserInformationNotProvided + } + assertNull(OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun excludedAadstsCode_50097_FilteredFromResponse() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("50097") // DeviceAuthenticationRequired + } + assertNull(OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun excludedAadstsCode_50126_FilteredFromResponse() { + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("50126") // InvalidUserNameOrPassword + } + assertNull(OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun excludedAadstsCode_AsSubError_AlsoFiltered() { + // Even when the excluded code is in the sub-error position, it is filtered. + // This also means the parser falls through to the (non-excluded) top-level error. + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("65001") + setCliTelemSubErrorCode("50126") + } + assertEquals("65001", OnboardingBlockingErrorParser.extractBlockingError(response)) + } + + @Test + fun excludedAadstsCode_FilteredFromHeader() { + val header = "1,50058,0,1234,routinghint" + assertNull(OnboardingBlockingErrorParser.extractBlockingError(header)) + } + + @Test + fun nonExcludedAadstsCode_StillReturned() { + // Sanity check: 65001 is a real onboarding-related blocker (interaction_required-ish). + // It must still pass through the filter. + val response = MicrosoftTokenResponse().apply { + setCliTelemErrorCode("65001") + } + assertEquals("65001", OnboardingBlockingErrorParser.extractBlockingError(response)) + } } From 02286fa6786521d54953e5b59856bbf2ad2a1a5b Mon Sep 17 00:00:00 2001 From: wzhipan Date: Wed, 13 May 2026 15:41:25 -0700 Subject: [PATCH 3/7] Add multi-value overload for OAuth error_codes parameter (Path B) eSTS commonly emits multiple AADSTS codes in the OAuth 'error_codes' query parameter on authorization redirect failures (e.g. '50058,53003' = no SSO session AND CA-blocked). The single-value overloads return only the most-specific identifier from the x-ms-clitelem header; for Path B we want all qualifying codes since the schema's blocking_errors[] is already an array and callers can invoke addBlockingError once per code. extractBlockingErrorsFromAuthorizationErrorCodes filters: - empty entries (trailing commas) - the literal '0' (eSTS no-error sentinel) - codes in NON_ONBOARDING_AADSTS_CODES (50058, 50097, 50126) - duplicates (preserves first-occurrence order) Adds 10 tests covering null/blank/single/multiple/excluded/all-excluded/ zero-sentinel/empty-entries/whitespace/duplicates cases. --- .../OnboardingBlockingErrorParser.kt | 29 +++++++ .../OnboardingBlockingErrorParserTest.kt | 86 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt index 6a7a8a368f..1d2a4d8fa9 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt @@ -123,4 +123,33 @@ object OnboardingBlockingErrorParser { return null } + + /** + * Extract blocking-error attribution codes from the OAuth `error_codes` query parameter + * of an authorization redirect (Microsoft extension to OAuth — comma-separated AADSTS codes, + * available on `MicrosoftStsAuthorizationErrorResponse.getErrorCodes()`). + * + * Unlike the single-value overloads (which return the most-specific identifier from the + * `x-ms-clitelem` header), eSTS frequently emits multiple AADSTS codes here when one + * authorization failure has multiple contributing causes (e.g. `"50058,53003"` = + * "no SSO session AND CA-blocked"). Returning all qualified codes lets callers add + * each via [com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder.addBlockingError] + * without losing attribution detail; the schema's `blocking_errors[]` is already an array. + * + * Filters out: + * - empty entries (e.g. trailing commas) + * - the literal `"0"` (eSTS's "no error" sentinel) + * - codes in [NON_ONBOARDING_AADSTS_CODES] + * - duplicates (preserves first-occurrence order) + * + * @return ordered list of qualifying AADSTS codes; empty if none qualify + */ + @JvmStatic + fun extractBlockingErrorsFromAuthorizationErrorCodes(errorCodes: String?): List { + if (errorCodes.isNullOrBlank()) return emptyList() + return errorCodes.split(",") + .map { it.trim() } + .filter { it.isNotEmpty() && it != "0" && !isExcluded(it) } + .distinct() + } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt index 463ec90dcc..8a8f35ae58 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt @@ -23,6 +23,7 @@ package com.microsoft.identity.common.java.telemetry import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResponse +import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -154,4 +155,89 @@ class OnboardingBlockingErrorParserTest { } assertEquals("65001", OnboardingBlockingErrorParser.extractBlockingError(response)) } + + // --- extractBlockingErrorsFromAuthorizationErrorCodes (Path B / OAuth error_codes) --- + + @Test + fun authzErrorCodes_NullReturnsEmpty() { + Assert.assertTrue( + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes(null).isEmpty() + ) + } + + @Test + fun authzErrorCodes_BlankReturnsEmpty() { + Assert.assertTrue( + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("").isEmpty() + ) + Assert.assertTrue( + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes(" ").isEmpty() + ) + } + + @Test + fun authzErrorCodes_SingleCodeReturned() { + Assert.assertEquals( + listOf("53003"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("53003") + ) + } + + @Test + fun authzErrorCodes_MultipleCodesAllReturnedInOrder() { + // eSTS commonly emits multiple codes when one failure has multiple causes. + Assert.assertEquals( + listOf("53003", "65001"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("53003,65001") + ) + } + + @Test + fun authzErrorCodes_ExcludedCodesFilteredOut() { + // 50058 is in the non-onboarding whitelist; 53003 is a real CA block. + Assert.assertEquals( + listOf("53003"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("50058,53003") + ) + } + + @Test + fun authzErrorCodes_AllExcludedReturnsEmpty() { + Assert.assertTrue( + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("50058,50097,50126").isEmpty() + ) + } + + @Test + fun authzErrorCodes_ZeroSentinelFilteredOut() { + Assert.assertEquals( + listOf("53003"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("0,53003") + ) + } + + @Test + fun authzErrorCodes_EmptyEntriesFilteredOut() { + // Trailing/leading commas → empty entries → ignored. + Assert.assertEquals( + listOf("53003", "65001"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes(",53003,,65001,") + ) + } + + @Test + fun authzErrorCodes_WhitespaceTrimmed() { + Assert.assertEquals( + listOf("53003", "65001"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes(" 53003 , 65001 ") + ) + } + + @Test + fun authzErrorCodes_DuplicatesDeduped() { + Assert.assertEquals( + listOf("53003", "65001"), + OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("53003,65001,53003") + ) + } } From d5fa19173e73659af06e933a99e55ce544ecef9c Mon Sep 17 00:00:00 2001 From: wzhipan Date: Mon, 18 May 2026 10:05:24 -0700 Subject: [PATCH 4/7] Changelog: onboarding blocking-error parser --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 6471f1c004..131cb5c40d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Add OnboardingBlockingErrorParser bridging x-ms-clitelem and OAuth error_codes into the onboarding telemetry blob, with non-onboarding AADSTS filtering and a multi-value Path B overload - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) From fa2c034ca40a1c2d821abf083a2790002ea4101d Mon Sep 17 00:00:00 2001 From: wzhipan Date: Wed, 20 May 2026 11:48:39 -0700 Subject: [PATCH 5/7] Address review comments on PR #3122 - Fix malformed KDoc reference in OnboardingBlockingErrorParser (extra ']' after AbstractMicrosoftStsTokenResponseHandler) - Align KDoc field names with the actual snake_case blob schema (last_blocking_error / blocking_errors) and reference the constants - Update the attribution precedence docs to match the implementation: the parser prefers serverSubErrorCode (most specific) and falls back to serverErrorCode, rather than the previous wording that implied errorCode was canonical - Test: rename 'Non-onboarding AADSTS code whitelist' to 'exclusion list' to reflect that the codes are being filtered out, not allowed - Changelog: add PR reference #3122 to the new vNext entry --- changelog.txt | 2 +- .../java/telemetry/OnboardingBlockingErrorParser.kt | 11 +++++++---- .../telemetry/OnboardingBlockingErrorParserTest.kt | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 131cb5c40d..a848d4868c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ vNext ---------- -- [MINOR] Add OnboardingBlockingErrorParser bridging x-ms-clitelem and OAuth error_codes into the onboarding telemetry blob, with non-onboarding AADSTS filtering and a multi-value Path B overload +- [MINOR] Add OnboardingBlockingErrorParser bridging x-ms-clitelem and OAuth error_codes into the onboarding telemetry blob, with non-onboarding AADSTS filtering and a multi-value Path B overload (#3122) - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt index 1d2a4d8fa9..3864c59efc 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParser.kt @@ -27,7 +27,8 @@ import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResp /** * Bridges eSTS-emitted error codes (carried via the `x-ms-clitelem` header and parsed * into [MicrosoftTokenResponse] by [com.microsoft.identity.common.java.providers.microsoft.microsoftsts.AbstractMicrosoftStsTokenResponseHandler]) - * into the onboarding telemetry blob's `lastBlockingError` / `blockingErrors` fields. + * into the onboarding telemetry blob's `last_blocking_error` / `blocking_errors` fields + * (see [OnboardingTelemetryConstants.LAST_BLOCKING_ERROR] / [OnboardingTelemetryConstants.BLOCKING_ERRORS]). * * Callers (OneAuth navigation fragment, broker error handler, etc.) invoke * [extractBlockingError] on the [MicrosoftTokenResponse] of a failed token request @@ -36,9 +37,11 @@ import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenResp * * Returns null when the response carries no error or the error is not blocking. * - * Per design spec (Mobile Onboarding Telemetry §10), `errorCode` from position 2 of the - * `x-ms-clitelem` header is the canonical attribution source for blocking errors. - * Sub-error code (position 3) is also captured for finer-grained classification. + * Per design spec (Mobile Onboarding Telemetry §10), the `x-ms-clitelem` header + * supplies both an `errorCode` (position 2) and a `subErrorCode` (position 3). + * This parser prefers the sub-error code when present (it is the most specific + * attribution signal) and falls back to the server error code; both are surfaced + * to the onboarding blob via `addBlockingError`. */ object OnboardingBlockingErrorParser { diff --git a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt index 8a8f35ae58..d9e5cc51f7 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt +++ b/common4j/src/test/com/microsoft/identity/common/java/telemetry/OnboardingBlockingErrorParserTest.kt @@ -103,7 +103,7 @@ class OnboardingBlockingErrorParserTest { assertNull(OnboardingBlockingErrorParser.extractBlockingError(header)) } - // --- Non-onboarding AADSTS code whitelist --- + // --- Non-onboarding AADSTS code exclusion list --- @Test fun excludedAadstsCode_50058_FilteredFromResponse() { @@ -194,7 +194,7 @@ class OnboardingBlockingErrorParserTest { @Test fun authzErrorCodes_ExcludedCodesFilteredOut() { - // 50058 is in the non-onboarding whitelist; 53003 is a real CA block. + // 50058 is in the non-onboarding exclusion list; 53003 is a real CA block. Assert.assertEquals( listOf("53003"), OnboardingBlockingErrorParser.extractBlockingErrorsFromAuthorizationErrorCodes("50058,53003") From 8ffe10052c8a54125def9442388df047e3500e23 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Thu, 21 May 2026 16:57:19 -0700 Subject: [PATCH 6/7] Re-trigger CI (previous run had infra-only failures: flaky coroutine test + corrupted Robolectric JAR cache + equal-coverage compare bug) From 597df946d86bbf4d34f845d674bd1fd83a815290 Mon Sep 17 00:00:00 2001 From: wzhipan <40340482+wzhipan@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:43:43 -0700 Subject: [PATCH 7/7] Move OnboardingBlockingErrorParser changelog entry from Version 24.3.0 (already shipped) to vNext (#3122) --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 6e55f8f666..5e84c25e74 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Add OnboardingBlockingErrorParser bridging x-ms-clitelem and OAuth error_codes into the onboarding telemetry blob, with non-onboarding AADSTS filtering and a multi-value Path B overload (#3122) - [PATCH] Fix incorrect Chrome Dev (com.chrome.dev) signing certificate thumbprint in AppRegistry (#3153) - [MINOR] Add IS_SWITCH_BROWSER_FLOW flag to PropertyBag so OneAuth can detect when the switch browser protocol was used during interactive auth (#3145) - [MINOR] Add Microsoft Defender for Endpoint (MDE) to AppRegistry authorized app lists (GET_DEVICE_TOKEN, DEVICE_REGISTRATION) (#3149) @@ -19,7 +20,6 @@ Version 24.3.0 - [PATCH] Emit ipc_strategy telemetry attribute for successful device registration IPC strategy and refactor execute flow to pack protocol request once before strategy retries (#3124) - [PATCH] Fix Edge browser selection on devices where Microsoft Edge is the default browser: add the rotated Edge signing certificate hash to the Edge BrowserDescriptor and accept multi-signer browsers when any signature intersects the safelist, instead of requiring strict set-equality (resolves MSAL #2414) - [MINOR] Refactor Auth Tab integration to use provider-based strategy selection. Adds AuthTabStrategyProvider and BrowserLaunchStrategy with Custom Tabs fallback. Compatible with androidx.browser:browser:1.7.0. -- [MINOR] Add OnboardingBlockingErrorParser bridging x-ms-clitelem and OAuth error_codes into the onboarding telemetry blob, with non-onboarding AADSTS filtering and a multi-value Path B overload (#3122) - [MINOR] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient for page-transition step capture (broker install, MDM enrollment, Company Portal launch, MFA linking) and last-loaded-domain tracking (#3121) - [MINOR] Propagate the onboarding telemetry blob through the broker failure path: add BaseException.onboardingBlob and round-trip it through MsalBrokerResultAdapter so callers can emit onboarding telemetry on failure outcomes. Also add an MsalBrokerResultAdapter overload that accepts an onboarding blob on the success path so the broker can attach the finalized blob to the success result bundle (#3123) - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086)