Skip to content

Commit e94bc62

Browse files
p3dr0rvCopilot
andauthored
Add additional allowed origins for PasskeyWebListener, Fixes AB#3465377 (#2839)
This pull request updates the list of allowed origins in the `PasskeyWebListener` class to support additional sovereign cloud identity endpoints. This change improves compatibility with new regional identity services. Expansion of allowed origins: * Added support for three new sovereign cloud identity endpoints: `login.sovcloud-identity.fr`, `login.sovcloud-identity.de`, and `login.sovcloud-identity.sg` to the allowed origins list in `PasskeyWebListener.kt`. [AB#3465377](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3465377) --------- Co-authored-by: Copilot <[email protected]>
1 parent 7ea5c92 commit e94bc62

File tree

8 files changed

+480
-234
lines changed

8 files changed

+480
-234
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [MINOR] Add additional allowed origins for PasskeyWebListener (#2839)
34
- [PATCH] Add JavascriptInterface rules to consumer proguard rules (#2837)
45
- [MINOR] Add optimized saveAndLoadAggregatedAccountData method in BrokerOAuth2TokenCache (#2832)
56
- [MINOR] Remove MavenCentral repository from build.gradle files (#2830)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.internal.providers.oauth2
24+
25+
import com.microsoft.identity.common.BuildConfig
26+
import com.microsoft.identity.common.logging.Logger
27+
import java.net.URI
28+
29+
/**
30+
* Manages allowed origin rules for Passkey/WebAuthN APIs.
31+
* Ensures only trusted Microsoft origins can access sensitive credential operations.
32+
*
33+
* **Origin Categories:**
34+
* 1. **Production origins** - Allow all paths (any path is safe)
35+
* 2. **Sovereign cloud origins** - Require "/fido" as a path segment
36+
*
37+
* **Security Features:**
38+
* - Validates scheme must be HTTPS
39+
* - Prevents subdomain spoofing (e.g., attacker.com.microsoft.com)
40+
* - Prevents prefix attacks (e.g., evillogin.microsoft.com)
41+
* - Requires exact domain matching
42+
* - Case-insensitive FIDO path matching for sovereign clouds
43+
*/
44+
object PasskeyOriginRulesManager {
45+
private const val TAG = "PasskeyOriginRulesManager"
46+
private const val HTTPS_SCHEME = "https"
47+
private const val FIDO_SEGMENT = "fido"
48+
49+
// Production origins - allow any path
50+
private val PRODUCTION_ORIGINS = setOf(
51+
"https://login.microsoft.com",
52+
"https://account.live.com",
53+
"https://mysignins.microsoft.com",
54+
"https://mysignins.azure.us",
55+
"https://mysignins.microsoft.scloud",
56+
"https://mysignins.eaglex.ic.gov"
57+
)
58+
59+
// Sovereign cloud origins - require FIDO path
60+
private val SOVEREIGN_CLOUD_ORIGINS = setOf(
61+
"https://login.microsoftonline.us",
62+
"https://login.microsoftonline.microsoft.scloud",
63+
"https://login.microsoftonline.eaglex.ic.gov",
64+
"https://login.sovcloud-identity.fr",
65+
"https://login.sovcloud-identity.de",
66+
"https://login.sovcloud-identity.sg"
67+
)
68+
69+
// PPE origins
70+
private val ALLOWED_ORIGIN_PPE= setOf(
71+
"https://account.live-int.com",
72+
"https://login.windows-ppe.net",
73+
"https://mysignins-ppe.microsoft.com"
74+
)
75+
76+
/**
77+
* Checks if the provided URL is allowed to access Passkey/WebAuthN APIs.
78+
*
79+
* **Validation Rules:**
80+
* - URL must have HTTPS scheme
81+
* - Host must exactly match an allowed origin
82+
* - For sovereign cloud origins, the path must contain "fido" as a complete segment
83+
* (e.g., "/fido" or "/fido/endpoint" are valid, but "/fidoauth" is not)
84+
* - For production origins, any path is allowed
85+
*
86+
* @param url The URL to validate
87+
* @return `true` if the URL is from an allowed origin and meets all validation rules, `false` otherwise
88+
*/
89+
@JvmStatic
90+
fun isAllowedOrigin(url: String): Boolean {
91+
return try {
92+
val uri = URI(url)
93+
94+
// Validate scheme
95+
if (uri.scheme.lowercase() != HTTPS_SCHEME) {
96+
return false
97+
}
98+
99+
// Validate host and build origin
100+
val host = uri.host ?: return false
101+
val origin = "https://$host".lowercase()
102+
103+
// Check if it's a production origin (any path allowed)
104+
if (PRODUCTION_ORIGINS.contains(origin)){
105+
return true
106+
}
107+
108+
// Check if it's a sovereign cloud origin (requires FIDO path)
109+
if (SOVEREIGN_CLOUD_ORIGINS.contains(origin)) {
110+
return hasFidoPathSegment(uri.path)
111+
}
112+
113+
false
114+
} catch (throwable: Throwable) {
115+
Logger.error(TAG, "Error validating origin for URL.", throwable)
116+
false
117+
}
118+
}
119+
120+
/**
121+
* Checks if the path contains "fido" as a complete path segment.
122+
*
123+
* Valid examples:
124+
* - "/fido" ✓
125+
* - "/fido/" ✓
126+
* - "/fido/endpoint" ✓
127+
* - "/some/path/fido" ✓
128+
* - "/some/path/fido/endpoint" ✓
129+
*
130+
* Invalid examples:
131+
* - "" ✗
132+
* - "/fidoauth" ✗ (fido is not a complete segment)
133+
* - "/authenticate" ✗ (no fido)
134+
*
135+
* @param path The path to validate (e.g., "/fido" or "/some/fido/endpoint")
136+
* @return `true` if path contains "fido" as a complete segment, `false` otherwise
137+
*/
138+
private fun hasFidoPathSegment(path: String?): Boolean {
139+
if (path.isNullOrEmpty()) {
140+
return false
141+
}
142+
143+
// Split by '/' and check for "fido" (case-insensitive)
144+
val segments = path.split("/")
145+
return segments.any { it.equals(FIDO_SEGMENT, ignoreCase = true) }
146+
}
147+
148+
/**
149+
* Returns the set of all allowed origin rules.
150+
*
151+
* @return Set containing all production and sovereign cloud origin URLs
152+
*/
153+
fun getAllowedOriginRules(): Set<String> {
154+
return if (BuildConfig.DEBUG) {
155+
PRODUCTION_ORIGINS + SOVEREIGN_CLOUD_ORIGINS + ALLOWED_ORIGIN_PPE
156+
} else {
157+
PRODUCTION_ORIGINS + SOVEREIGN_CLOUD_ORIGINS
158+
}
159+
}
160+
}

common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -289,38 +289,8 @@ class PasskeyWebListener(
289289
var __webauthn_interface__,__webauthn_hooks__;!function(e){__webauthn_interface__.addEventListener("message",(function(e){console.log(e.data);var n=JSON.parse(e.data);"get"===n.type?o(n):"create"===n.type?l(n):console.log("Incorrect response format for reply: "+n.type)}));var n=null,t=null,r=null,a=null;function o(e){if(null!==n&&null!==r){if("success"!=e.status){var o=r;return n=null,r=null,void o(new DOMException(e.data.domExceptionMessage,e.data.domExceptionName))}var s=u(e.data),i=n;n=null,r=null,i(s)}else console.log("Reply failure: Resolve: "+t+" and reject: "+a)}function s(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),(function(e){return e.charCodeAt(0)})).buffer}function i(e){return btoa(Array.from(new Uint8Array(e),(function(e){return String.fromCharCode(e)})).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function l(e){if(null!==t&&null!==a){if("success"!=e.status){var n=a;return t=null,a=null,void n(new DOMException(e.data.domExceptionMessage,e.data.domExceptionName))}var r=u(e.data),o=t;t=null,a=null,o(r)}else console.log("Reply failure: Resolve: "+t+" and reject: "+a)}function u(e){return e.rawId=s(e.rawId),e.response.clientDataJSON=s(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=s(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=s(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=s(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=s(e.response.userHandle)),e.getClientExtensionResults=function(){return{}},e.response.getTransports=function(){return e.response.hasOwnProperty("transports")?e.response.transports:[]},e}e.create=function(n){if(!("publicKey"in n))return e.originalCreateFunction(n);var r=new Promise((function(e,n){t=e,a=n})),o=n.publicKey;if(o.hasOwnProperty("challenge")){var s=i(o.challenge);o.challenge=s}if(o.hasOwnProperty("user")&&o.user.hasOwnProperty("id")){var l=i(o.user.id);o.user.id=l}if(o.hasOwnProperty("excludeCredentials")&&Array.isArray(o.excludeCredentials)&&o.excludeCredentials.length>0)for(var u=0;u<o.excludeCredentials.length;u++){var c=o.excludeCredentials[u];c&&c.hasOwnProperty("id")&&(c.id=i(c.id))}var p={type:"create",request:o},_=JSON.stringify(p);return __webauthn_interface__.postMessage(_),r},e.get=function(t){if(!("publicKey"in t))return e.originalGetFunction(t);var a=new Promise((function(e,t){n=e,r=t})),o=t.publicKey;if(o.hasOwnProperty("challenge")){var s=i(o.challenge);o.challenge=s}var l={type:"get",request:o},u=JSON.stringify(l);return __webauthn_interface__.postMessage(u),a},e.onReplyGet=o,e.CM_base64url_decode=s,e.CM_base64url_encode=i,e.onReplyCreate=l}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!0)};
290290
"""
291291

292-
/** Allowed origins that can use the WebAuthN interface. */
293-
private val ALLOWED_ORIGIN_RULES_PRODUCTION = setOf(
294-
"https://login.microsoft.com",
295-
"https://account.live.com",
296-
"https://mysignins.microsoft.com",
297-
"https://mysignins.azure.us",
298-
"https://mysignins.microsoft.scloud",
299-
"https://mysignins.eaglex.ic.gov",
300-
"https://login.microsoftonline.us",
301-
"https://login.microsoftonline.microsoft.scloud",
302-
"https://login.microsoftonline.eaglex.ic.gov"
303-
)
304292

305-
/** Allowed origins for pre-production/testing environments. */
306-
private val ALLOWED_ORIGIN_PRE_PRODUCTION = setOf(
307-
"https://account.live-int.com",
308-
"https://login.windows-ppe.net",
309-
"https://mysignins-ppe.microsoft.com"
310-
)
311293

312-
/**
313-
* Gets the set of allowed origin rules based on build configuration.
314-
*
315-
* @return Set of allowed origin rules.
316-
*/
317-
private fun getAllowedOriginRules(): Set<String> {
318-
val mutableSet = ALLOWED_ORIGIN_RULES_PRODUCTION.toMutableSet()
319-
if (BuildConfig.DEBUG) {
320-
mutableSet.addAll(ALLOWED_ORIGIN_PRE_PRODUCTION)
321-
}
322-
return mutableSet.toSet()
323-
}
324294

325295
/**
326296
* Attaches the passkey listener to a WebView.
@@ -357,7 +327,7 @@ class PasskeyWebListener(
357327
WebViewCompat.addWebMessageListener(
358328
webView,
359329
INTERFACE_NAME,
360-
getAllowedOriginRules(),
330+
PasskeyOriginRulesManager.getAllowedOriginRules(),
361331
PasskeyWebListener(
362332
coroutineScope = CoroutineScope(Dispatchers.Default),
363333
credentialManagerHandler = CredentialManagerHandler(activity)
@@ -373,11 +343,7 @@ class PasskeyWebListener(
373343
} else {
374344
WEB_AUTHN_INTERFACE_JS_MINIFIED
375345
}
376-
webClient.addOnPageStartedScript(
377-
TAG,
378-
scriptToInject,
379-
getAllowedOriginRules()
380-
)
346+
webClient.addPasskeyRegistrationJsScript(scriptToInject)
381347

382348
true
383349
} else {

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.microsoft.identity.common.internal.fido.IFidoManager;
5353
import com.microsoft.identity.common.internal.fido.LegacyFido2ApiManager;
5454
import com.microsoft.identity.common.internal.providers.oauth2.AuthorizationActivity;
55+
import com.microsoft.identity.common.internal.providers.oauth2.PasskeyOriginRulesManager;
5556
import com.microsoft.identity.common.internal.providers.oauth2.WebViewAuthorizationFragment;
5657
import com.microsoft.identity.common.internal.ui.webview.certbasedauth.AbstractSmartcardCertBasedAuthChallengeHandler;
5758
import com.microsoft.identity.common.internal.ui.webview.certbasedauth.AbstractCertBasedAuthChallengeHandler;
@@ -88,13 +89,11 @@
8889
import java.net.URISyntaxException;
8990
import java.net.URL;
9091
import java.security.Principal;
91-
import java.util.ArrayList;
9292
import java.util.Arrays;
9393
import java.util.HashMap;
9494
import java.util.List;
9595
import java.util.Locale;
9696
import java.util.Map;
97-
import java.util.Set;
9897
import java.util.concurrent.TimeUnit;
9998

10099
import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.AMAZON_APP_REDIRECT_PREFIX;
@@ -144,7 +143,7 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient {
144143
private final SpanContext mSpanContext;
145144
private final String mUtid;
146145

147-
private final List<JsScriptRecord> mOnPageStartedScripts = new ArrayList<>();
146+
private String mPasskeyRegistrationScript;
148147

149148
public AzureActiveDirectoryWebViewClient(@NonNull final Activity activity,
150149
@NonNull final IAuthorizationCompletionCallback completionCallback,
@@ -1144,12 +1143,10 @@ public void onReceived(@Nullable final AbstractCertBasedAuthChallengeHandler cha
11441143
@Override
11451144
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
11461145
super.onPageStarted(view, url, favicon);
1147-
// Evaluate JavaScript for each script if URL matches allowed origins
1148-
for (final JsScriptRecord scriptRecord : mOnPageStartedScripts) {
1149-
if (scriptRecord.isAllowedForUrl(url)) {
1150-
Logger.info(TAG, "Executing onPageStarted script: " + scriptRecord.getId());
1151-
view.evaluateJavascript(scriptRecord.getScript(), null);
1152-
}
1146+
// Evaluate JavaScript for Passkey Registration if script is set and origin is allowed.
1147+
if (mPasskeyRegistrationScript != null && PasskeyOriginRulesManager.isAllowedOrigin(url)) {
1148+
Logger.verbose(TAG, "Executing onPageStarted PasskeyRegistration script for URL: " + url);
1149+
view.evaluateJavascript(mPasskeyRegistrationScript, null);
11531150
}
11541151
}
11551152

@@ -1232,17 +1229,10 @@ private Span createSpanWithAttributesFromParent(@NonNull final String spanName)
12321229

12331230
/**
12341231
* Add a JavaScript to be executed in onPageStarted.
1235-
* If allowedUrls is null, the script will be executed for all URLs.
1236-
* If allowedUrls is non-null, the script will be executed only for URLs that start with any of the allowed origins.
1232+
* The script will be executed only for URLs that are allowed by {@link PasskeyOriginRulesManager}.
12371233
* @param script JavaScript code to be executed.
1238-
* @param allowedUrls Set of allowed URL origins.
12391234
*/
1240-
public void addOnPageStartedScript(
1241-
@NonNull final String scriptId,
1242-
@NonNull final String script,
1243-
@Nullable final Set<String> allowedUrls) {
1244-
this.mOnPageStartedScripts.add(
1245-
new JsScriptRecord(scriptId, script, allowedUrls)
1246-
);
1235+
public void addPasskeyRegistrationJsScript(@NonNull final String script) {
1236+
this.mPasskeyRegistrationScript = script;
12471237
}
12481238
}

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/JsScriptRecord.kt

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)