-
Notifications
You must be signed in to change notification settings - Fork 49
Add BrowserRedirectValidator and integrate into AndroidAuthorizationStrategy, Fixes AB#3568776 #3070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
15
commits into
dev
Choose a base branch
from
copilot/ab-3568776-add-browser-redirect-validator
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+566
−0
Open
Add BrowserRedirectValidator and integrate into AndroidAuthorizationStrategy, Fixes AB#3568776 #3070
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2663623
Initial plan
Copilot 2ea28a5
Add BrowserRedirectValidator and integrate into AuthorizationActivity…
Copilot ebe7752
Address code review feedback: use setData instead of setDataAndNormal…
Copilot beb1cb1
Fix ClientException propagation: validate in launchIntent() for activ…
Copilot 515a8d1
Remove optional context from factory; consolidate URL scheme validati…
Copilot 7aacf48
Remove unused Context import and TAG constant from AuthorizationActiv…
Copilot c63cd77
Merge branch 'dev' into copilot/ab-3568776-add-browser-redirect-valid…
fadidurah 331af58
Remove excessive Logger.warn before throw in BrowserRedirectValidator
Copilot ab48aa0
Remove duplicate test coverage from AuthorizationActivityFactoryTest
Copilot 66c0c20
Fix test intent registration to include CATEGORY_DEFAULT and CATEGORY…
Copilot 81521f2
Merge branch 'dev' into copilot/ab-3568776-add-browser-redirect-valid…
fadidurah ec642cb
Merge branch 'dev' into copilot/ab-3568776-add-browser-redirect-valid…
fadidurah 64a048c
Fix compilation error: use RawAuthorizationResult in completeAuthoriz…
Copilot 62aaa55
Remove trailing blank line in AuthorizationActivityFactoryTest.java
Copilot 50c4672
Fix raw type compilation errors in AndroidAuthorizationStrategyValida…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
.../java/com/microsoft/identity/common/internal/providers/oauth2/BrowserRedirectValidator.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // 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.internal.providers.oauth2 | ||
|
|
||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.content.pm.PackageManager | ||
| import android.net.Uri | ||
| import com.microsoft.identity.common.java.exception.ClientException | ||
| import com.microsoft.identity.common.java.exception.ErrorStrings | ||
|
|
||
| /** | ||
| * Validates that no other application is registered to handle the same custom URL scheme | ||
| * used for the BrowserTabActivity redirect URI. | ||
| */ | ||
| object BrowserRedirectValidator { | ||
|
|
||
| private const val BROWSER_TAB_ACTIVITY_CLASS = | ||
| "com.microsoft.identity.client.BrowserTabActivity" | ||
| private const val CURRENT_TASK_BROWSER_TAB_ACTIVITY_CLASS = | ||
| "com.microsoft.identity.client.CurrentTaskBrowserTabActivity" | ||
|
|
||
| /** | ||
| * Verifies that no other application is listening on the custom URL scheme defined by | ||
| * [redirectUri]. If another application's activity is found that handles the same scheme, | ||
| * a [ClientException] with error code | ||
| * [ErrorStrings.MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME] is thrown. | ||
| * | ||
| * @param context The Android context used to query the PackageManager. | ||
| * @param redirectUri The redirect URI whose URL scheme will be checked. | ||
| * @param useCurrentTask Whether the flow uses [CurrentTaskBrowserTabActivity] (true) or | ||
| * [BrowserTabActivity] (false) as the expected activity class. | ||
| * @throws ClientException if another application is found listening on the same URL scheme. | ||
| */ | ||
| @JvmStatic | ||
| @Throws(ClientException::class) | ||
| fun validateNoMultipleAppsListening( | ||
| context: Context, | ||
| redirectUri: String, | ||
| useCurrentTask: Boolean | ||
| ) { | ||
| val packageManager = context.packageManager ?: return | ||
|
|
||
| val intent = Intent(Intent.ACTION_VIEW).apply { | ||
| addCategory(Intent.CATEGORY_DEFAULT) | ||
| addCategory(Intent.CATEGORY_BROWSABLE) | ||
| data = Uri.parse(redirectUri) | ||
| } | ||
|
|
||
| val resolvedActivities = packageManager.queryIntentActivities( | ||
| intent, | ||
| PackageManager.GET_RESOLVED_FILTER | ||
| ) | ||
|
|
||
| val expectedActivityClassName = if (useCurrentTask) { | ||
| CURRENT_TASK_BROWSER_TAB_ACTIVITY_CLASS | ||
| } else { | ||
| BROWSER_TAB_ACTIVITY_CLASS | ||
| } | ||
|
|
||
| for (resolveInfo in resolvedActivities) { | ||
| val activityInfo = resolveInfo.activityInfo ?: continue | ||
| // If this is our own registered BrowserTabActivity, it is expected — skip it. | ||
| if (activityInfo.name == expectedActivityClassName && | ||
| activityInfo.packageName == context.packageName | ||
| ) { | ||
| continue | ||
| } | ||
| // Another application's activity is also listening on this URL scheme. | ||
| val otherPackage = activityInfo.packageName | ||
| throw ClientException( | ||
|
fadidurah marked this conversation as resolved.
|
||
| ErrorStrings.MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME, | ||
| "More than one app is listening for the URL scheme defined for BrowserTabActivity " + | ||
| "in the AndroidManifest. The package name of this other app is: $otherPackage" | ||
| ) | ||
| } | ||
| } | ||
| } | ||
184 changes: 184 additions & 0 deletions
184
...t/identity/common/internal/providers/oauth2/AndroidAuthorizationStrategyValidationTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // 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.internal.providers.oauth2 | ||
|
|
||
| import android.app.Activity | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.content.pm.ActivityInfo | ||
| import android.content.pm.ResolveInfo | ||
| import android.net.Uri | ||
| import com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.AUTHORIZATION_AGENT | ||
| import com.microsoft.identity.common.adal.internal.AuthenticationConstants.AuthorizationIntentKey.REDIRECT_URI | ||
| import com.microsoft.identity.common.java.exception.ClientException | ||
| import com.microsoft.identity.common.java.exception.ErrorStrings | ||
| import com.microsoft.identity.common.java.providers.oauth2.AuthorizationRequest | ||
| import com.microsoft.identity.common.java.providers.oauth2.OAuth2Strategy | ||
| import com.microsoft.identity.common.java.ui.AuthorizationAgent | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.fail | ||
| import org.junit.Before | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import org.robolectric.Robolectric | ||
| import org.robolectric.RobolectricTestRunner | ||
| import org.robolectric.RuntimeEnvironment | ||
| import org.robolectric.Shadows.shadowOf | ||
|
|
||
| /** | ||
| * Tests that [AndroidAuthorizationStrategy.launchIntent] performs URL scheme conflict validation | ||
| * for non-WebView flows and skips it for WebView flows. | ||
| */ | ||
| @RunWith(RobolectricTestRunner::class) | ||
| class AndroidAuthorizationStrategyValidationTest { | ||
|
|
||
| companion object { | ||
| private const val COMPETING_PACKAGE = "com.example.otherapp" | ||
| private const val COMPETING_ACTIVITY = "com.example.otherapp.SomeActivity" | ||
| private const val REDIRECT_URI_VALUE = "msauth://org.robolectric.default/redirect" | ||
| } | ||
|
|
||
| private lateinit var activity: Activity | ||
| private lateinit var context: Context | ||
|
|
||
| @Before | ||
| fun setUp() { | ||
| activity = Robolectric.buildActivity(Activity::class.java).create().get() | ||
| context = RuntimeEnvironment.getApplication() | ||
| } | ||
|
|
||
| /** | ||
| * Minimal concrete subclass of [AndroidAuthorizationStrategy] that exposes [launchIntent] | ||
| * publicly for testing (and overrides [requestAuthorization] as required by the interface). | ||
| */ | ||
| @Suppress("UNCHECKED_CAST") | ||
| private inner class TestAndroidAuthorizationStrategy( | ||
| appContext: Context, | ||
| act: Activity | ||
| ) : AndroidAuthorizationStrategy< | ||
| OAuth2Strategy<*, *, *, *, *, *, *, *, *, *, *, *, *>, | ||
| AuthorizationRequest<*>>(appContext, act, null) { | ||
|
|
||
| override fun requestAuthorization( | ||
| authorizationRequest: AuthorizationRequest<*>, | ||
| oAuth2Strategy: OAuth2Strategy<*, *, *, *, *, *, *, *, *, *, *, *, *> | ||
| ) = throw UnsupportedOperationException("not used in these tests") | ||
|
|
||
| override fun completeAuthorization(requestCode: Int, data: com.microsoft.identity.common.java.providers.RawAuthorizationResult) = | ||
| Unit | ||
|
|
||
| // Expose the protected method publicly for tests. | ||
| @Throws(ClientException::class) | ||
| fun testLaunchIntent(intent: Intent) = launchIntent(intent) | ||
| } | ||
|
|
||
| // ────────────────────────────────────────────────────────────────────────────────── | ||
| // Helper | ||
| // ────────────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| private fun registerCompetingApp() { | ||
| val resolveInfo = ResolveInfo().apply { | ||
| activityInfo = ActivityInfo().apply { | ||
| packageName = COMPETING_PACKAGE | ||
| name = COMPETING_ACTIVITY | ||
| } | ||
| } | ||
| val competingRedirectIntent = Intent(Intent.ACTION_VIEW).apply { | ||
| data = Uri.parse(REDIRECT_URI_VALUE) | ||
| addCategory(Intent.CATEGORY_DEFAULT) | ||
| addCategory(Intent.CATEGORY_BROWSABLE) | ||
| } | ||
| shadowOf(context.packageManager).addResolveInfoForIntent( | ||
| competingRedirectIntent, resolveInfo | ||
| ) | ||
| } | ||
|
|
||
| private fun buildIntent(agent: AuthorizationAgent?, redirectUri: String? = REDIRECT_URI_VALUE): Intent { | ||
| val intent = Intent() | ||
| if (agent != null) intent.putExtra(AUTHORIZATION_AGENT, agent) | ||
| if (redirectUri != null) intent.putExtra(REDIRECT_URI, redirectUri) | ||
| return intent | ||
| } | ||
|
|
||
| // ────────────────────────────────────────────────────────────────────────────────── | ||
| // Tests | ||
| // ────────────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Browser flow with a competing app registered → [ClientException] must be thrown from | ||
| * [launchIntent] so it propagates through the command pipeline. | ||
| */ | ||
| @Test | ||
| fun `launchIntent browser flow throws ClientException when competing app is registered`() { | ||
| registerCompetingApp() | ||
| val strategy = TestAndroidAuthorizationStrategy(context, activity) | ||
|
|
||
| try { | ||
| strategy.testLaunchIntent(buildIntent(AuthorizationAgent.BROWSER)) | ||
| fail("Expected ClientException") | ||
| } catch (e: ClientException) { | ||
| assertEquals(ErrorStrings.MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME, e.errorCode) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * WebView flow with a competing app registered → no exception (WebView is excluded from | ||
| * URL scheme conflict validation). | ||
| */ | ||
| @Test | ||
| fun `launchIntent webview flow does not throw even when competing app is registered`() { | ||
| registerCompetingApp() | ||
| val strategy = TestAndroidAuthorizationStrategy(context, activity) | ||
|
|
||
| // Should not throw — activity.startActivity() will simply be called. | ||
| strategy.testLaunchIntent(buildIntent(AuthorizationAgent.WEBVIEW)) | ||
| } | ||
|
|
||
| /** | ||
| * Null authorizationAgent is treated as a browser flow → validation runs, and a conflict | ||
| * causes [ClientException]. | ||
| */ | ||
| @Test | ||
| fun `launchIntent null authorizationAgent treated as browser flow and throws on conflict`() { | ||
| registerCompetingApp() | ||
| val strategy = TestAndroidAuthorizationStrategy(context, activity) | ||
|
|
||
| try { | ||
| strategy.testLaunchIntent(buildIntent(agent = null)) | ||
| fail("Expected ClientException") | ||
| } catch (e: ClientException) { | ||
| assertEquals(ErrorStrings.MULTIPLE_APPS_LISTENING_CUSTOM_URL_SCHEME, e.errorCode) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Browser flow with no competing apps → no exception. | ||
| */ | ||
| @Test | ||
| fun `launchIntent browser flow passes when no competing app is registered`() { | ||
| val strategy = TestAndroidAuthorizationStrategy(context, activity) | ||
|
|
||
| // No competing app registered — should not throw (startActivity completes normally). | ||
| strategy.testLaunchIntent(buildIntent(AuthorizationAgent.BROWSER)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.