-
Notifications
You must be signed in to change notification settings - Fork 36
Expose a JavaScript API in brokered Webviews to facilitate Improved Same Device NumberMatch , Fixes AB#3203956 #2617
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
base: dev
Are you sure you want to change the base?
Changes from all commits
c4d3c81
1ffa139
243ccd7
ccfe0d2
2945a5f
2bd13c0
f478f41
caf538c
5d26097
ee7a7c7
8ae18e1
bb18c6d
b8ee1ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// 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.broker | ||
|
||
import android.webkit.JavascriptInterface | ||
import com.google.gson.stream.MalformedJsonException | ||
import com.microsoft.identity.common.internal.numberMatch.NumberMatchHelper | ||
import com.microsoft.identity.common.java.util.JsonUtil | ||
import com.microsoft.identity.common.logging.Logger | ||
|
||
/** | ||
* JavaScript API to receive JSON string payloads from AuthUX in order to facilitate calling various | ||
* broker methods. | ||
*/ | ||
class AuthUxJavaScriptInterface { | ||
|
||
// Store number matches in a static hash map | ||
// No need to persist this storage beyond the current broker process, but we need to keep them | ||
// long enough for AuthApp to call the broker api to fetch the number match | ||
companion object { | ||
val TAG = AuthUxJavaScriptInterface::class.java.simpleName | ||
private const val JAVASCRIPT_INTERFACE_NAME = "ClientBrokerJS" | ||
|
||
fun getInterfaceName() : String { | ||
return JAVASCRIPT_INTERFACE_NAME | ||
} | ||
} | ||
|
||
/** | ||
* Method to receive a JSON string payload from AuthUX through JavaScript API. | ||
* Schema for the Json Payload: | ||
* { | ||
* "correlationID": "SOME_CORRELATION_ID" , | ||
* "action_name":"write_data", | ||
* "action_component":"broker", | ||
* "params": | ||
* { | ||
* "function": "NUMBER_MATCH", | ||
* "data": | ||
* { | ||
* "sessionID": "$mockSessionId", | ||
* "numberMatch": "$mockNumberMatchValue" | ||
* } | ||
* } | ||
* } | ||
* TODO: This is currently the schema set for numberMatch, there may be some additions made for | ||
* the more generalized JSON Schema for future Server-side to broker communication through JS. | ||
* | ||
* https://microsoft-my.sharepoint-df.com/:w:/p/veenasoman/EY1AZIeT8X5KrXVz97Vx520B3Jj0fBLSPlklnoRvcmbh0Q?e=VzNFd1&ovuser=72f988bf-86f1-41af-91ab-2d7cd011db47%2Cfadidurah%40microsoft.com&clickparams=eyJBcHBOYW1lIjoiVGVhbXMtRGVza3RvcCIsIkFwcFZlcnNpb24iOiI0OS8yNTA1MDQwMTYwOSIsIkhhc0ZlZGVyYXRlZFVzZXIiOmZhbHNlfQ%3D%3D | ||
*/ | ||
@JavascriptInterface | ||
fun postMessageToBroker(jsonPayload: String) { | ||
val methodTag = "$TAG:postMessageToBroker" | ||
Logger.info(methodTag, "Received a payload from AuthUX through JavaScript API.") | ||
|
||
try { | ||
val parsedJson = JsonUtil.extractJsonObjectIntoMap(jsonPayload) | ||
|
||
val correlationID = parsedJson["correlationID"] | ||
Logger.info(methodTag, "Correlation ID during JavaScript Call: [$correlationID]") | ||
|
||
// TODO: Leaving these here, as these will be relevant for next WebCP feature | ||
// val actionName = parsedJson["action_name"] | ||
// val actionComponent = parsedJson["action_component"] | ||
|
||
val parameters = JsonUtil.extractJsonObjectIntoMap(parsedJson["params"]) | ||
val function = parameters["function"] | ||
val data = JsonUtil.extractJsonObjectIntoMap(parameters["data"]) | ||
Logger.info(methodTag, "Function name: [$function]") | ||
|
||
when (function) { | ||
FunctionNames.NUMBER_MATCH.name -> | ||
NumberMatchHelper.storeNumberMatch( | ||
data[NumberMatchHelper.SESSION_ID_ATTRIBUTE_NAME], | ||
data[NumberMatchHelper.NUMBER_MATCH_ATTRIBUTE_NAME]) | ||
else -> | ||
Logger.warn(methodTag, "Payload from AuthUX contained an unknown function name.") | ||
} | ||
} catch (e: Exception) { // If we run into exceptions, we don't want to kill the broker | ||
when (e) { | ||
is NullPointerException -> { | ||
Logger.warn(methodTag, "Payload with missing mandatory fields sent through JavaScriptInterface") | ||
} | ||
is MalformedJsonException -> { | ||
Logger.warn(methodTag, "Malformed JSON payload sent through JavaScriptInterface") | ||
} | ||
else -> { | ||
Logger.warn(methodTag, "Unknown error occurred while processing the payload.") | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Enum class to hold function names | ||
*/ | ||
enum class FunctionNames { | ||
NUMBER_MATCH | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// 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.numberMatch | ||
|
||
import com.microsoft.identity.common.logging.Logger | ||
|
||
/** | ||
* Helper to facilitate NumberMatchFlow. Used in conjunction with {@link AuthUxJavaScriptInterface} | ||
* When authenticator is installed, and phone uses MFA or PSI in an interactive flow, a number | ||
* matching challenge is issued, where used is given a number and asked to open authenticator and check | ||
* for the same number in authenticator UI. This feature cuts out one UI step, where this API is used to | ||
* supply the number match value and store it in ephemeral storage (kept as long as current broker | ||
* process is alive), where Authenticator can call a broker API to fetch the number match, and immediately | ||
* prompt user for consent, rather than first asking them to check the number match. | ||
*/ | ||
class NumberMatchHelper { | ||
|
||
// Store number matches in a static hash map | ||
// No need to persist this storage beyond the current broker process, but we need to keep them | ||
// long enough for AuthApp to call the broker api to fetch the number match | ||
companion object { | ||
val TAG = NumberMatchHelper::class.java.simpleName | ||
val numberMatchMap: HashMap<String, String> = HashMap() | ||
const val SESSION_ID_ATTRIBUTE_NAME = "sessionID" | ||
const val NUMBER_MATCH_ATTRIBUTE_NAME = "numberMatch" | ||
|
||
/** | ||
* Method to add a key:value pair of sessionID:numberMatch to static hashmap. This hashmap will be accessed | ||
* by broker api to get the number match for a particular sessionID. | ||
*/ | ||
fun storeNumberMatch(sessionId: String?, numberMatch: String?) { | ||
val methodTag = "$TAG:storeNumberMatch" | ||
Logger.info(methodTag, | ||
"Adding entry in NumberMatch hashmap for session ID: $sessionId") | ||
|
||
// If both parameters are non-null, add a new entry to the hashmap | ||
if (sessionId != null && numberMatch != null) { | ||
numberMatchMap[sessionId] = numberMatch | ||
} | ||
// If either parameter is null, do nothing | ||
else { | ||
Logger.warn(methodTag, | ||
"Either session ID or number match is null. Nothing to add for number match." | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Clear existing number match key:value pairs | ||
*/ | ||
fun clearNumberMatchMap() { | ||
numberMatchMap.clear() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ | |
import android.view.View; | ||
import android.view.ViewGroup; | ||
import android.webkit.PermissionRequest; | ||
import android.webkit.ValueCallback; | ||
import android.webkit.WebChromeClient; | ||
import android.webkit.WebSettings; | ||
import android.webkit.WebView; | ||
|
@@ -47,7 +48,9 @@ | |
import com.microsoft.identity.common.R; | ||
import com.microsoft.identity.common.internal.fido.LegacyFidoActivityResultContract; | ||
import com.microsoft.identity.common.internal.fido.LegacyFido2ApiObject; | ||
import com.microsoft.identity.common.internal.broker.AuthUxJavaScriptInterface; | ||
import com.microsoft.identity.common.internal.ui.webview.ISendResultCallback; | ||
import com.microsoft.identity.common.internal.ui.webview.ProcessUtil; | ||
import com.microsoft.identity.common.internal.ui.webview.switchbrowser.SwitchBrowserProtocolCoordinator; | ||
import com.microsoft.identity.common.java.WarningType; | ||
import com.microsoft.identity.common.adal.internal.AuthenticationConstants; | ||
|
@@ -122,6 +125,9 @@ public class WebViewAuthorizationFragment extends AuthorizationFragment { | |
// This is used by the switch browser protocol to handle the resume of the flow. | ||
private SwitchBrowserProtocolCoordinator mSwitchBrowserProtocolCoordinator = null; | ||
|
||
private boolean isBrokerRequest = false; | ||
private boolean isEstsRequest = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH, I don't think we make requests anywhere besides eSTS so all of our requests are to eSTS. So not sure what this check is doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May be you intend to check for the "current" URL that we are navigating to within the webview? I think that would be ideal but I don't think your code is doing that at the moment |
||
|
||
@Override | ||
public void onCreate(@Nullable Bundle savedInstanceState) { | ||
super.onCreate(savedInstanceState); | ||
|
@@ -211,6 +217,12 @@ void extractState(@NonNull final Bundle state) { | |
mAuthIntent = state.getParcelable(AUTH_INTENT); | ||
mPkeyAuthStatus = state.getBoolean(PKEYAUTH_STATUS, false); | ||
mAuthorizationRequestUrl = state.getString(REQUEST_URL); | ||
if (mAuthorizationRequestUrl != null) { | ||
isEstsRequest = mAuthorizationRequestUrl.startsWith("https://login.microsoftonline.com"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about sovereign clouds? |
||
} | ||
if (getContext() != null) { | ||
isBrokerRequest = ProcessUtil.isRunningOnAuthService(getContext()); | ||
} | ||
mRedirectUri = state.getString(REDIRECT_URI); | ||
mRequestHeaders = getRequestHeaders(state); | ||
mPostPageLoadedJavascript = state.getString(POST_PAGE_LOADED_URL); | ||
|
@@ -290,6 +302,9 @@ private void setUpWebView(@NonNull final View view, | |
mWebView.getSettings().setUserAgentString( | ||
userAgent + AuthenticationConstants.Broker.CLIENT_TLS_NOT_SUPPORTED); | ||
mWebView.getSettings().setJavaScriptEnabled(true); | ||
if (isBrokerRequest && isEstsRequest) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens once the URL changes within the webview? So we go from eSTS to ADFS for federated auth but the |
||
mWebView.addJavascriptInterface(new AuthUxJavaScriptInterface(), AuthUxJavaScriptInterface.Companion.getInterfaceName()); | ||
} | ||
mWebView.requestFocus(View.FOCUS_DOWN); | ||
|
||
// Set focus to the view for touch event | ||
|
@@ -398,7 +413,7 @@ private HashMap<String, String> getRequestHeaders(final Bundle state) { | |
} | ||
|
||
// Attach client extras header for ESTS telemetry. Only done for broker requests | ||
if (isBrokerRequest(this.mAuthorizationRequestUrl)) { | ||
if (isBrokerRequest) { | ||
final ClientExtraSku clientExtraSku = ClientExtraSku.builder() | ||
.srcSku(state.getString(PRODUCT)) | ||
.srcSkuVer(state.getString(VERSION)) | ||
|
@@ -415,15 +430,7 @@ private HashMap<String, String> getRequestHeaders(final Bundle state) { | |
public ActivityResultLauncher<LegacyFido2ApiObject> getFidoLauncher() { | ||
return mFidoLauncher; | ||
} | ||
|
||
/** | ||
* Helper method to check if the authorization request is being made through broker. | ||
* Done by checking for broker version key in the url | ||
*/ | ||
private boolean isBrokerRequest(final String authorizationUrl) { | ||
return authorizationUrl.contains(Device.PlatformIdParameters.BROKER_VERSION); | ||
} | ||
|
||
|
||
class AuthorizationCompletionCallback implements IAuthorizationCompletionCallback { | ||
@Override | ||
public void onChallengeResponseReceived(@NonNull final RawAuthorizationResult response) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// 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.broker | ||
|
||
import com.microsoft.identity.common.internal.numberMatch.NumberMatchHelper | ||
import org.junit.Before | ||
import org.junit.Test | ||
|
||
class AuthUxJavaScriptInterfaceTest { | ||
|
||
private lateinit var authUxJavaScriptInterface: AuthUxJavaScriptInterface | ||
|
||
private val mockSessionId = "1234" | ||
private val mockNumberMatchValue = "00" | ||
|
||
private val numberMatchTestPayload = """ | ||
{ | ||
"correlationID": "SOME_CORRELATION_ID" , | ||
"action_name":"write_data", | ||
"action_component":"broker", | ||
"params": | ||
{ | ||
"function": "NUMBER_MATCH", | ||
"data": | ||
{ | ||
"sessionID": "$mockSessionId", | ||
"numberMatch": "$mockNumberMatchValue" | ||
} | ||
} | ||
} | ||
""".trimIndent() | ||
|
||
@Before | ||
fun setUp() { | ||
authUxJavaScriptInterface = AuthUxJavaScriptInterface() | ||
} | ||
|
||
@Test | ||
fun `test postMessageToBroker with NUMBER_MATCH function`() { | ||
// Call the method | ||
authUxJavaScriptInterface.postMessageToBroker(numberMatchTestPayload) | ||
|
||
// Verify that the data was stored in NumberMatchHelper | ||
val storedValue = NumberMatchHelper.numberMatchMap[mockSessionId] | ||
assert(storedValue == mockNumberMatchValue) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's define a DTO representing the json schema and then you can just use the kotlinx serialization libraries (or even GSON or Moshi if you prefer that) to deserialize the raw json directly to a custom dto.
For instance:
and then you can deserialize as follows using kotlinx serialization framework
and then you can access individual properties as follows:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! I can adjust to this, this current Json parsing implementation is a bit janky