feat: eSTS cloud host detection and PRT header re-attachment on WebView redirects, Fixes AB#3568709#3071
feat: eSTS cloud host detection and PRT header re-attachment on WebView redirects, Fixes AB#3568709#3071
Conversation
|
✅ Work item link check complete. Description contains link AB#3568709 to an Azure Boards work item. |
1 similar comment
|
✅ Work item link check complete. Description contains link AB#3568709 to an Azure Boards work item. |
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
| private val headers: HashMap<String, String>, | ||
| private val span : Span | ||
| private val span : Span, | ||
| private val fallbackUsername: String? = null |
There was a problem hiding this comment.
@copilot , there are compile time errors due to adding private val fallbackUsername: String? = null in ReAttachPrtHeaderHandler
There was a problem hiding this comment.
Fixed in commit 9ebf18d. The root cause was that Kotlin default constructor parameters aren't visible to Java callers without @JvmOverloads. Added @JvmOverloads to both ReAttachPrtHeaderHandler and NonceRedirectHandler constructors — this generates the 3-argument overload Java expects at the processCrossCloudRedirect and processWebCpAuthorize call sites.
…t (#AB3568709) Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/f41fadc2-0e92-457b-9714-23f4f6df8312 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
… exception catch, improve tests Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/f41fadc2-0e92-457b-9714-23f4f6df8312 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
… and related identifiers Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/78e45bd4-5958-4e41-9f10-eaadb93b8b4f Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
…tached guard Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/3df0bcf0-ff0c-410c-ad11-23d4c47b5a58 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
…so_nonce redirects When an eSTS redirect URL contains sso_nonce (a common scenario), isNonceRedirect fires before isEstsCloudHost in the handleUrl() chain. NonceRedirectHandler now accepts an optional fallbackUsername (passed as mLoginHint from the initial authorize URL), so PRT re-attachment with the new nonce succeeds even when login_hint is absent from the redirect URL. Adds NonceRedirectHandlerTest covering the fallback path and the no-fallback guard. Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/cb6db2b2-56e8-4f9d-ac48-45d3d1aaa520 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
…andler constructors Kotlin default constructor parameters are not visible to Java callers without @jvmoverloads. The two 3-argument Java call sites of ReAttachPrtHeaderHandler (processCrossCloudRedirect, processWebCpAuthorize) were getting compile errors because no 3-arg overload existed. @jvmoverloads generates the necessary overloaded constructors for Java interop. Same annotation added to NonceRedirectHandler for consistency. Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/f90300a1-6ff8-4474-b169-3d5683a0722e Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
The feature applies to any eSTS cloud host navigation, not just return-from-external redirects. Rename the flight constant, string key, and Javadoc to reflect the actual behavior.
Move the 3-part condition (flight check + eSTS host check + PRT header check) into a single method for readability in the handleUrl() chain.
- Rename hasMatchingClientId to hasKnownClientId, compare redirect client_id against both original request client_id and BROKER_CLIENT_ID constant - Return false (skip PRT) when /authorize redirect has no client_id - Add getRefreshTokenCredentialWithBrokerPurpose() to IRefreshTokenCredentialProvider - Wire purpose:broker path through CommonRefreshTokenCredentialProvider - ReAttachPrtHeaderHandler: add withPurposeBroker flag, only used for eSTS host redirects - Add lazy getLoginHintFromRequestUrl() and getClientIdFromRequestUrl() extractors
99f44b1 to
481e848
Compare
- Remove fallbackUsername parameter from NonceRedirectHandler constructor - Update NonceRedirectHandlerTest to use 3-param constructor - Remove unused FALLBACK_USERNAME constant from tests - Fix pre-existing compile error: add throws ClientException to EstsHostRedirectTest.setup()
…try-catch, consolidate tests
There was a problem hiding this comment.
Pull request overview
Adds a new flighted WebView path in the Common Android auth library to detect navigations back to an eSTS cloud host and re-attach a fresh PRT credential header (with additional telemetry), aiming to prevent SSO loss after redirects to external Entra domains.
Changes:
- Introduces a new OpenTelemetry span/attribute and a new CommonFlight flag to gate eSTS-host PRT header re-attachment.
- Extends PRT credential generation support with an optional “purpose:broker” JWT path and integrates it into WebView redirect handling.
- Adds/updates Robolectric tests covering eSTS host redirect handling and nonce redirect behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java | Adds a new span name for eSTS-host PRT header re-attachment telemetry. |
| common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java | Adds a new attribute to record the eSTS redirect host for telemetry. |
| common4j/src/main/com/microsoft/identity/common/java/interfaces/IRefreshTokenCredentialProvider.kt | Adds a new API intended to obtain a refresh token credential JWT with purpose:broker. |
| common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java | Adds a new flight to gate the eSTS-host PRT re-attachment behavior. |
| common4j/src/main/com/microsoft/identity/common/java/broker/CommonRefreshTokenCredentialProvider.kt | Wires through the new “broker purpose” credential getter. |
| common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/ReAttachPrtHeaderHandler.kt | Adds fallback username support and an option to request a purpose:broker credential. |
| common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java | Adds eSTS cloud host detection + gated PRT header re-attachment on redirect navigations. |
| common/src/test/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/NonceRedirectHandlerTest.kt | Adds tests around nonce redirect handling and header re-attachment behavior. |
| common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientEstsHostRedirectTest.java | Adds tests for the new flighted eSTS-host redirect handling logic. |
| /** | ||
| * Span name for PRT header re-attachment when WebView redirects to an eSTS cloud host. | ||
| */ | ||
| EstsHostRedirectPrtAttach, |
There was a problem hiding this comment.
The new span name is EstsHostRedirectPrtAttach, but the PR description/requirements reference an “EstsReturnRedirectPrtAttach” span for the return-to-eSTS redirect scenario. Impact: telemetry dashboards/alerts that expect the documented span name won’t light up, and cross-repo consistency can break. Recommendation: align the enum value name (and its usage sites) with the final intended span name, or update the PR description/spec so they match.
| // - redirectClientId == null → false | ||
| // - redirectClientId matches originalClientId (from mRequestUrl) → true | ||
| // - redirectClientId matches BROKER_CLIENT_ID → true | ||
| // - exception during extraction → true |
There was a problem hiding this comment.
The comment block describing hasKnownClientId() behavior says “exception during extraction → true”, but the current implementation of hasKnownClientId() returns false on exceptions. Impact: the test documentation is misleading for future maintainers and can cause incorrect assumptions when updating the gating logic. Recommendation: update the comment to reflect the actual behavior (exception → false), or adjust the production code if the intended behavior is different.
| // - exception during extraction → true | |
| // - exception during extraction → false |
| @Test | ||
| fun `processChallenge attaches PRT credential when login_hint is in URL`() { | ||
| mockkObject(CommonRefreshTokenCredentialProvider) | ||
| every { | ||
| CommonRefreshTokenCredentialProvider.getRefreshTokenCredentialUsingNewNonce( | ||
| ESTS_URL_WITH_NONCE_AND_HINT, "user@example.com", "testnonce" | ||
| ) | ||
| } returns FRESH_CREDENTIAL | ||
|
|
||
| val handler = NonceRedirectHandler(webView, headers, span) | ||
| handler.processChallenge(URL(ESTS_URL_WITH_NONCE_AND_HINT)) | ||
|
|
||
| verify(span).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true) | ||
| verify(webView).loadUrl(ESTS_URL_WITH_NONCE_AND_HINT, headers) | ||
| } |
There was a problem hiding this comment.
In the success-path test, the provider is stubbed to return a fresh credential, but the test only verifies span attribute + loadUrl call and never asserts that the PRT header value in headers was actually updated to the fresh credential. Impact: the test could pass even if the handler forgets to write the updated header value. Recommendation: assert that headers[PRT_RESPONSE_HEADER] equals the expected refreshed credential after processChallenge().
There was a problem hiding this comment.
Done in commit 5200dc9. Added assertEquals(FRESH_CREDENTIAL, headers[AuthenticationConstants.Broker.PRT_RESPONSE_HEADER]) to the success-path test so it will now fail if the handler omits writing the updated credential to the headers map.
| private boolean shouldReAttachPrtForEstsHost(@NonNull final String url) { | ||
| return CommonFlightsManager.INSTANCE.getFlightsProvider() | ||
| .isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT) | ||
| && isEstsCloudHost(url) | ||
| && hasPrtHeaderAttached() | ||
| && (!url.contains("/authorize") || hasKnownClientId(url)); | ||
| } |
There was a problem hiding this comment.
shouldReAttachPrtForEstsHost() uses url.contains("/authorize") on the original (non-lowercased) URL string. Impact: this check is case-sensitive and can misclassify URLs (and also matches query string content), causing the client_id gating to be applied (or skipped) incorrectly and leading to either missed PRT re-attachment or overly broad attachment. Recommendation: parse the URL and check the path segment (similar to isWebCpAuthorizeUrl()) and/or run the check on a lowercased/path-only value rather than doing a raw substring search on the full URL.
| } catch (final Throwable e) { | ||
| Logger.warn(methodTag, "Failed to process eSTS host redirect. Falling back to loading URL without PRT." + e); | ||
| if (span != null) { |
There was a problem hiding this comment.
The warning log concatenates the caught Throwable into a non-PII Logger.warn message (..." + e). For URL parsing failures, exception messages can include the full URL/query, which may contain PII (e.g., login_hint). Impact: potential sensitive data leakage into non-PII logs. Recommendation: avoid appending the exception to the warn string; rely on span.recordException(e) for diagnostics, and if logging is needed, log a redacted URL (removeQueryParametersOrRedact(url)) and/or use a PII-safe logger path.
| val username = parameters["login_hint"] ?: fallbackUsername | ||
| if (!username.isNullOrEmpty()) { | ||
| val updatedRefreshTokenCredentialHeader = | ||
| val updatedRefreshTokenCredentialHeader = if (withPurposeBroker) { | ||
| CommonRefreshTokenCredentialProvider.getRefreshTokenCredentialWithBrokerPurpose( | ||
| url, username | ||
| ) | ||
| } else { | ||
| CommonRefreshTokenCredentialProvider.getRefreshTokenCredential( | ||
| url, username | ||
| ) | ||
| } |
There was a problem hiding this comment.
ReAttachPrtHeaderHandler now has new behavior branches (fallbackUsername and withPurposeBroker) but existing unit tests only exercise the default path calling getRefreshTokenCredential(). Impact: regressions in fallback username handling or purpose:broker JWT generation could ship unnoticed. Recommendation: add unit tests that (1) verify fallbackUsername is used when login_hint is absent in the redirect URL, and (2) verify withPurposeBroker=true calls getRefreshTokenCredentialWithBrokerPurpose and updates the PRT header + telemetry attribute.
…success path The success-path test only verified span attribute and loadUrl calls but never asserted that headers[PRT_RESPONSE_HEADER] was actually updated to the fresh credential. Added assertEquals(FRESH_CREDENTIAL, headers[PRT_RESPONSE_HEADER]) so the test will catch any regression where the handler fails to write the updated header value. Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/b10d63cb-fbe7-4144-9666-acc873c4eab0 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
handleUrl()branches that can match eSTShttps://URLs beforeisEstsCloudHostNonceRedirectHandlerto accept optionalfallbackUsernameparametermLoginHintas fallback toNonceRedirectHandlerinprocessNonceAndReAttachHeadersNonceRedirectHandlerTestcovering fallback username scenarios@JvmOverloadstoReAttachPrtHeaderHandlerandNonceRedirectHandlerconstructorsheaders[PRT_RESPONSE_HEADER]equals the fresh credential inNonceRedirectHandlerTestsuccess-path test