Skip to content

Add BrowserRedirectValidator and integrate into AndroidAuthorizationStrategy, Fixes AB#3568776#3070

Open
Copilot wants to merge 15 commits intodevfrom
copilot/ab-3568776-add-browser-redirect-validator
Open

Add BrowserRedirectValidator and integrate into AndroidAuthorizationStrategy, Fixes AB#3568776#3070
Copilot wants to merge 15 commits intodevfrom
copilot/ab-3568776-add-browser-redirect-validator

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 7, 2026

Multiple-apps-listening-on-same-URL-scheme validation previously ran at PCA initialization time in MSAL, surfacing errors immediately at startup before any token request was made. This caused false positives in broker flows where the browser path is never taken. This PR moves the check to browser authorization launch time (inside acquireToken) so it only fires when the browser authorization path is actually exercised.

Timing tradeoff: Because the check no longer runs at PCA init, redirect URI conflicts are now caught later — only when the user initiates an interactive browser-based sign-in. Apps that use broker without explicitly setting authorization_user_agent to BROWSER will no longer see false-positive errors at startup, but the tradeoff is that genuine URI conflicts will be detected during acquireToken rather than at initialization.

Changes

common4j — ErrorStrings

  • Added MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME = "multiple_apps_listening_url_scheme" constant

common — BrowserRedirectValidator.kt (new)

Kotlin object with validateNoMultipleAppsListening(context, redirectUri, useCurrentTask):

  • Builds an ACTION_VIEW intent with DEFAULT/BROWSABLE categories and the redirect URI as data
  • Queries PackageManager.queryIntentActivities(intent, GET_RESOLVED_FILTER) for all matching handlers
  • Skips the app's own BrowserTabActivity (or CurrentTaskBrowserTabActivity when useCurrentTask=true)
  • Throws ClientException(MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME, ...) on any other matching activity
  • Returns without throwing if PackageManager is null

common — AuthorizationActivityFactory.kt

  • Factory is now a pure fragment-creation utility with no validation logic and no optional context parameter — no caller can inadvertently skip the check
  • WebView and SilentWebView paths return the correct fragment type unchanged

common — AndroidAuthorizationStrategy.java

  • All URL scheme conflict validation is consolidated in launchIntent() — the single point where throws ClientException is declared — covering both the fragment-embedded and full-Activity paths with a single upfront check
  • WebView flows are explicitly excluded from validation
  • A null authorizationAgent is treated as a browser flow (default behavior)
  • Warning logs emitted when context or redirect URI is null (validation skipped)
// In launchIntent(), before fragment/activity branching:
final AuthorizationAgent authorizationAgent = (AuthorizationAgent)
    intent.getSerializableExtra(AUTHORIZATION_AGENT);
if (authorizationAgent != AuthorizationAgent.WEBVIEW) {
    // validate — throws ClientException on conflict
}

Exception propagation to MsalClientException

ClientException thrown by the validator flows: launchIntent() (throws ClientException) → requestAuthorization()acquireToken()InteractiveTokenCommand.execute()CommandDispatcher.executeCommand() (caught as BaseException) → MSAL's callback adapter → MsalClientException.

Tests

  • BrowserRedirectValidatorTest.kt (new): 9 unit tests covering pass/fail/edge cases including null PackageManager, empty resolved list, useCurrentTask flag selecting the correct activity class, and competing-app detection. Test intent registration mirrors the validator's intent shape (CATEGORY_DEFAULT + CATEGORY_BROWSABLE) to ensure Robolectric shadow PM resolution matches the real query.
  • AuthorizationActivityFactoryTest.java: Tests confirming the factory returns the correct fragment type for WebView and browser agents (validation is not the factory's concern); duplicate tests removed.
  • AndroidAuthorizationStrategyValidationTest.kt (new): 4 tests verifying that browser flows with a competing app throw ClientException from launchIntent(), WebView flows are skipped, null agent is treated as browser, and no-conflict flows pass. Competing-app registration uses the correct intent shape (CATEGORY_DEFAULT + CATEGORY_BROWSABLE) to match the validator's query.

AB#3568776

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

✅ Work item link check complete. Description contains link AB#3568776 to an Azure Boards work item.

@github-actions github-actions bot changed the title [WIP] Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory [WIP] Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory, Fixes AB#3568776 Apr 7, 2026
@github-actions github-actions bot changed the title [WIP] Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory [WIP] Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory, Fixes AB#3568776 Apr 7, 2026
Copilot AI and others added 2 commits April 7, 2026 08:15
…Factory

- Add MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME constant to ErrorStrings.java (common4j)
- Create BrowserRedirectValidator.kt: validates no other app listens on the same
  custom URL scheme used for BrowserTabActivity redirect URI
- Modify AuthorizationActivityFactory.getAuthorizationFragmentFromStartIntent() to
  accept optional Context and call the validator before browser fragment creation
- Update callers (AuthorizationActivity, CurrentTaskAuthorizationActivity,
  AndroidAuthorizationStrategy) to pass context for validation
- Add BrowserRedirectValidatorTest.kt with comprehensive unit tests
- Extend AuthorizationActivityFactoryTest.java with validator integration tests

Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/5ea3f2f4-472e-4f76-8658-091921409a78

Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
…ize, add null redirectUri warning log, extract test constants

Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/5ea3f2f4-472e-4f76-8658-091921409a78

Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
Copilot AI changed the title [WIP] Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory, Fixes AB#3568776 Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory Apr 7, 2026
Copilot AI requested a review from fadidurah April 7, 2026 08:26
@github-actions github-actions bot changed the title Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory, Fixes AB#3568776 Apr 7, 2026
…ity path, not in Activity.onCreate()

The ClientException thrown by BrowserRedirectValidator was unhandled when validation
ran inside AuthorizationActivity/CurrentTaskAuthorizationActivity.onCreate(), since Android
framework lifecycle methods have no throws clause. The exception would crash the Activity
with no proper MsalClientException conversion.

Fix: move the validation for the activity-based flow into AndroidAuthorizationStrategy.launchIntent(),
where 'throws ClientException' is declared and the exception can propagate correctly through the
command pipeline to MSAL's adapter layer (where it becomes MsalClientException).

- Fragment path (fragment != null): validation still runs inside the factory call
  (context is passed from launchIntent()), exception propagates through launchIntent() ✓
- Activity path (fragment == null): validation now runs directly in launchIntent() BEFORE
  startActivity(), exception propagates through launchIntent() ✓
- AuthorizationActivity.onCreate() / CurrentTaskAuthorizationActivity.onCreate(): no longer
  pass context to the factory, so no unhandled ClientException risk there

Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/df3b75d4-7602-47da-948a-b3421cf0cf2b

Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
…on in launchIntent()

- AuthorizationActivityFactory.kt: remove context param and validation logic entirely.
  Factory is now a pure fragment-creation utility with no optional parameters that could
  allow callers to inadvertently skip the check.
- AndroidAuthorizationStrategy.launchIntent(): performs all validation upfront for both
  fragment and activity paths, WebView flows explicitly excluded. Adds warning logs for
  null context or null redirectUri instead of silently skipping.
- AuthorizationActivityFactoryTest.java: remove validator-specific tests (now covered by
  BrowserRedirectValidatorTest and new AndroidAuthorizationStrategyValidationTest). Simplify
  factory tests to pure fragment-type assertions.
- AndroidAuthorizationStrategyValidationTest.kt (new): 4 tests covering browser-conflict
  throws, WebView skips, null-agent-treated-as-browser, and no-conflict pass.

Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/a7303d99-19bd-4ab2-9221-cd24067b2c11

Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 7, 2026

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:

  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -XX:MaxMetaspaceSize=1024m -XX:&#43;HeapDumpOnOutOfMemoryError -Xmx3072m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant (dns block)
  • identitydivision.pkgs.visualstudio.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -XX:MaxMetaspaceSize=1024m -XX:&#43;HeapDumpOnOutOfMemoryError -Xmx3072m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant (dns block)
  • www.puppycrawl.com
    • Triggering command: /opt/hostedtoolcache/CodeQL/2.25.1/x64/codeql/tools/linux64/java/bin/java /opt/hostedtoolcache/CodeQL/2.25.1/x64/codeql/tools/linux64/java/bin/java -jar /opt/hostedtoolcache/CodeQL/2.25.1/x64/codeql/xml/tools/xml-extractor.jar --fileList=/tmp/codeql-scratch-b2678d0398b00160/dbs/java/working/files-to-index8943149093118371350.list --sourceArchiveDir=/tmp/codeql-scratch-b2678d0398b00160/dbs/java/src --outputDir=/tmp/codeql-scratch-b2678d0398b00160/dbs/java/trap/java (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Add BrowserRedirectValidator and integrate into AuthorizationActivityFactory, Fixes AB#3568776 Add BrowserRedirectValidator and integrate into AndroidAuthorizationStrategy, Fixes AB#3568776 Apr 7, 2026
…ityFactory

Both were leftover from the intermediate state where context was passed to
the factory. Now that all validation lives in AndroidAuthorizationStrategy.launchIntent(),
these are dead code. The only remaining change to this file is the Javadoc note
explaining why validation is performed upstream.

Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/dc9a6656-00d7-44f8-99e0-02c29adaa4b4

Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
Copilot AI requested a review from fadidurah April 10, 2026 02:22
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot I don't need you to comment on the PR, i am inquiring with you directly, on how to actually repro the scenario on the dev (original) code, if i take out package name from the manifest, will t...

Yes, the package visibility is the key. To reproduce on API 30+:

Option 1 (recommended): Add the Azure sample's package name to the MSAL test app's <queries> block in AndroidManifest.xml:

<queries>
    <package android:name="com.azure.sample" />  <!-- or whatever the Azure sample package is -->
    ...
</queries>

Once the target app is visible, queryIntentActivities with the scheme-only msauth:// intent will return its BrowserTabActivity, and the original check will throw.

Option 2: Test on a device/emulator running API < 30 — package visibility restrictions don't apply there, so all installed apps are visible without any <queries> declaration.

The <queries> declaration needs to be in the calling app (MSAL test app), not the target app, so removing/adding entries from the target app's manifest has no effect on this.

@fadidurah
Copy link
Copy Markdown
Contributor

@copilot update the Description, to note that the check previously happened during PCA initialization, but will now happen during the acquire token call. This means that the tradeoff for enabling brokered scenarios without changing authorization_user_agent is that we don't catch the error as early.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot update the Description, to note that the check previously happened during PCA initialization, but will now happen during the acquire token call. This means that the tradeoff for enabling brok...

Updated the PR description with the tradeoff note: the check previously ran during PCA initialization; it now runs during acquireToken (at browser authorization launch time). This means conflicts are caught later in the flow, but broker flows no longer produce false positives when authorization_user_agent isn't explicitly set to BROWSER.

@github-actions
Copy link
Copy Markdown

❌ Invalid work item number: AB#3568776. Work item number must be a valid integer.

Click here to learn more.

@fadidurah
Copy link
Copy Markdown
Contributor

@copilot there are compilation errors in AndroidAuthorizationStrategyValidationTest

}

}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove leftover whitespace

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 62aaa55.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot there are compilation errors in AndroidAuthorizationStrategyValidationTest

Fixed in 64a048c. The completeAuthorization override was using PropertyBag instead of the RawAuthorizationResult type required by IAuthorizationStrategy.

@fadidurah
Copy link
Copy Markdown
Contributor

@copilot still getting compilation errors for OAuth2Strategy and AuthorizationRequest declarations in TestAndroidAuthorizationStrategy. Seems there is some expected type arguements

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot still getting compilation errors for OAuth2Strategy and AuthorizationRequest declarations in TestAndroidAuthorizationStrategy. Seems there is some expected type arguements

Fixed in 50c4672. OAuth2Strategy and AuthorizationRequest are generic Java classes — using them as raw types in Kotlin produces "expected type arguments" errors. Replaced with star projections: OAuth2Strategy<*, *, *, *, *, *, *, *, *, *, *, *, *> (13 type params) and AuthorizationRequest<*> (1 type param) in both the supertype declaration and the requestAuthorization override.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants