diff --git a/changelog.txt b/changelog.txt index 8d83c92c14..0d8de7d0b3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Expose a JavaScript API in brokered Webviews to facilitate Improved Same Device NumberMatch (#2617) - [MINOR] Add API for resource account provisioning (API only) (#2640) Version 21.1.0 diff --git a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java index c0f95d8655..6a4854a9d7 100644 --- a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java +++ b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java @@ -1222,6 +1222,16 @@ public static String computeMaxHostBrokerProtocol() { */ public static final String REDIRECT_PREFIX = "msauth"; + /** + * Prefix for AAD urls + */ + public static final String AAD_URL_HOST_PREFIX = "login.microsoftonline."; + + /** + * Prefix for MSA urls + */ + public static final String MSA_URL_HOST_PREFIX = "login.live."; + /** * Encoded delimiter for redirect. */ diff --git a/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterface.kt b/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterface.kt new file mode 100644 index 0000000000..09978a5b86 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterface.kt @@ -0,0 +1,172 @@ +// 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.GsonBuilder +import com.google.gson.JsonParseException +import com.google.gson.JsonSyntaxException +import com.google.gson.stream.MalformedJsonException +import com.microsoft.identity.common.adal.internal.AuthenticationConstants +import com.microsoft.identity.common.internal.numberMatch.NumberMatchHelper +import com.microsoft.identity.common.logging.Logger +import java.net.MalformedURLException +import java.net.URL + +/** + * 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 + } + + /** + * Helper method to determine if url is a valid Url for the JS Interface + * @param url url being loaded + * @return true if url is a valid, safe url, false otherwise + */ + fun isValidUrlForInterface(urlString: String?): Boolean { + // If url is null, return false + if (urlString.isNullOrEmpty()) { + return false + } + + val url : URL + try { + url = URL(urlString) + } catch (e: MalformedURLException) { + // If url is not a valid URL, return false + Logger.warn(TAG, "Malformed URL passed.") + return false + + } + + val host = url.host + + // Otherwise, make sure url is a valid url + // We only want to allow URLs that have the AAD or MSA url hosts + return host.startsWith(AuthenticationConstants.Broker.AAD_URL_HOST_PREFIX) || + host.startsWith(AuthenticationConstants.Broker.MSA_URL_HOST_PREFIX) + } + } + + /** + * 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 payloadObject = parseJsonToAuthUxJsonPayloadObject(jsonPayload) + + Logger.info(methodTag, "Correlation ID during JavaScript Call: [${payloadObject.correlationId}]") + + + // TODO: Leaving these here, as these will be relevant for next WebCP feature + // val actionName = payloadObject.actionName + // val actionComponent = payloadObject.actionComponent + + val parameters = payloadObject.params + if (parameters == null) { + Logger.warn(methodTag, "Payload from AuthUX contained no \"params\" field.") + return + } + + val function = parameters.function + + Logger.info(methodTag, "Function name: [$function]") + + val data = parameters.data + if (data == null) { + Logger.warn(methodTag, "Payload from AuthUX contained no \"data\" field.") + return + } + + when (function) { + FunctionNames.NUMBER_MATCH.name -> + NumberMatchHelper.storeNumberMatch( + data.sessionId, + data.numberMatch) + 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.error(methodTag, "Payload with missing mandatory fields sent through JavaScriptInterface", e) + } + is MalformedJsonException, is JsonSyntaxException, is JsonParseException -> { + Logger.error(methodTag, "Error Parsing JSON payload sent through JavaScriptInterface", e) + } + else -> { + Logger.error(methodTag, "Unknown error occurred while processing the payload.", e) + } + } + } + } + + private fun parseJsonToAuthUxJsonPayloadObject(jsonString: String): AuthUxJsonPayload{ + val gson = GsonBuilder() + .registerTypeAdapter(AuthUxJsonPayload::class.java, AuthUxJsonPayloadKTDeserializer()) + .create() + return gson.fromJson(jsonString, AuthUxJsonPayload::class.java) + } + + /** + * Enum class to hold function names + */ + enum class FunctionNames { + NUMBER_MATCH + } +} diff --git a/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayload.kt b/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayload.kt new file mode 100644 index 0000000000..668f795522 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayload.kt @@ -0,0 +1,110 @@ +// 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.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.annotations.SerializedName +import java.lang.reflect.Type + +/** + * Data class representing the JSON payload object received from AuthUX. + * + * @property correlationId The correlation ID for the request. + * @property actionName The name of the action being performed. + * @property actionComponent The component responsible for the action. + * @property params The parameters for the action, including function and data. + */ +data class AuthUxJsonPayload( + val correlationId: String, + val actionName: String, + val actionComponent: String, + val params: AuthUxParams? +) + +/** + * Data class representing the parameters for the action, including function and data. + * + * @property function The function to be executed. + * @property data The data associated with the function. + */ +data class AuthUxParams( + @SerializedName(SerializedNames.FUNCTION) + val function: String?, + + @SerializedName(SerializedNames.DATA) + val data: AuthUxData? +) + +/** + * Data class representing the data associated with the JS API call. + * + * @property sessionId The session ID for the request. + * @property numberMatch The number match value. + */ +data class AuthUxData( + @SerializedName(SerializedNames.SESSION_ID) + val sessionId: String?, + + @SerializedName(SerializedNames.NUMBER_MATCH) + val numberMatch: String? +) + +class AuthUxJsonPayloadKTDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): AuthUxJsonPayload { + val jsonObject = json.asJsonObject + + // Validate required fields + val correlationId = jsonObject.get(SerializedNames.CORRELATION_ID)?.asString + ?: throw JsonParseException("correlationID is required and cannot be null") + val actionName = jsonObject.get(SerializedNames.ACTION_NAME)?.asString + ?: throw JsonParseException("action_name is required and cannot be null") + val actionComponent = jsonObject.get(SerializedNames.ACTION_COMPONENT)?.asString + ?: throw JsonParseException("action_component is required and cannot be null") + + // Deserialize params if present + val params = jsonObject.get("params")?.let { + context.deserialize(it, AuthUxParams::class.java) + } + + return AuthUxJsonPayload( + correlationId = correlationId, + actionName = actionName, + actionComponent = actionComponent, + params = params + ) + } +} + +object SerializedNames { + const val CORRELATION_ID = "correlationID" + const val ACTION_NAME = "action_name" + const val ACTION_COMPONENT = "action_component" + const val PARAMS = "params" + const val FUNCTION = "function" + const val DATA = "data" + const val SESSION_ID = "sessionID" + const val NUMBER_MATCH = "numberMatch" +} diff --git a/common/src/main/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelper.kt b/common/src/main/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelper.kt new file mode 100644 index 0000000000..3f2cca28c1 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelper.kt @@ -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 = 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() + } + } +} diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java index 80c0ce2aca..c55ab14959 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java @@ -24,6 +24,7 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.Build; @@ -33,6 +34,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 +49,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; @@ -58,7 +62,6 @@ import com.microsoft.identity.common.java.constants.FidoConstants; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.flighting.CommonFlight; -import com.microsoft.identity.common.java.platform.Device; import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback; import com.microsoft.identity.common.java.providers.RawAuthorizationResult; @@ -122,6 +125,8 @@ 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; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -211,6 +216,10 @@ void extractState(@NonNull final Bundle state) { mAuthIntent = state.getParcelable(AUTH_INTENT); mPkeyAuthStatus = state.getBoolean(PKEYAUTH_STATUS, false); mAuthorizationRequestUrl = state.getString(REQUEST_URL); + final Context context = getContext(); + if (context != null) { + isBrokerRequest = ProcessUtil.isRunningOnAuthService(context); + } mRedirectUri = state.getString(REDIRECT_URI); mRequestHeaders = getRequestHeaders(state); mPostPageLoadedJavascript = state.getString(POST_PAGE_LOADED_URL); @@ -256,6 +265,7 @@ public void onPageLoaded(final String url) { getSwitchBrowserCoordinator().getSwitchBrowserRequestHandler() ); setUpWebView(view, mAADWebViewClient); + mAADWebViewClient.initializeAuthUxJavaScriptApi(mWebView, mAuthorizationRequestUrl); launchWebView(mAuthorizationRequestUrl, mRequestHeaders); return view; } @@ -398,7 +408,7 @@ private HashMap 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)) @@ -416,14 +426,6 @@ public ActivityResultLauncher 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) { diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index 2db5879bcc..18a73c141b 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -41,6 +41,7 @@ import com.microsoft.identity.common.adal.internal.AuthenticationConstants; import com.microsoft.identity.common.adal.internal.util.StringExtensions; +import com.microsoft.identity.common.internal.broker.AuthUxJavaScriptInterface; import com.microsoft.identity.common.internal.broker.PackageHelper; import com.microsoft.identity.common.internal.fido.CredManFidoManager; import com.microsoft.identity.common.internal.fido.FidoChallenge; @@ -130,6 +131,19 @@ public AzureActiveDirectoryWebViewClient(@NonNull final Activity activity, mSwitchBrowserRequestHandler = switchBrowserRequestHandler; } + /** + * This method is used to initialize the JavaScript API for the AuthUx JavaScript interface. + * It checks if the current process is running on the AuthService and if the URL is valid for the interface. + * If both conditions are met, it adds the JavaScript interface to the WebView. + */ + public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final String url) { + if (ProcessUtil.isRunningOnAuthService(getActivity().getApplicationContext()) && AuthUxJavaScriptInterface.Companion.isValidUrlForInterface(url)) { + // If broker request, and a valid url, expose JavaScript API + Logger.info(TAG, "Adding AuthUx JavaScript Interface"); + view.addJavascriptInterface(new AuthUxJavaScriptInterface(), AuthUxJavaScriptInterface.Companion.getInterfaceName()); + } + } + /** * Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView. * This method was deprecated in API level 24. @@ -191,6 +205,18 @@ private boolean handleUrl(final WebView view, final String url) { final String methodTag = TAG + ":handleUrl"; final String formattedURL = url.toLowerCase(Locale.US); + // Re-evaluate adding AuthUx JavaScript Interface + if (ProcessUtil.isRunningOnAuthService(getActivity().getApplicationContext()) && AuthUxJavaScriptInterface.Companion.isValidUrlForInterface(url)) { + // If broker request, and a valid url, expose JavaScript API + Logger.info(methodTag, "Adding AuthUx JavaScript Interface"); + view.addJavascriptInterface(new AuthUxJavaScriptInterface(), AuthUxJavaScriptInterface.Companion.getInterfaceName()); + } else { + // Remove AuthUx JavaScript Interface + Logger.info(methodTag, "Removing AuthUx JavaScript Interface"); + view.removeJavascriptInterface(AuthUxJavaScriptInterface.Companion.getInterfaceName()); + } + + try { if (isPkeyAuthUrl(formattedURL)) { Logger.info(methodTag,"WebView detected request for pkeyauth challenge."); diff --git a/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterfaceTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterfaceTest.kt new file mode 100644 index 0000000000..fb5eb2c538 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJavaScriptInterfaceTest.kt @@ -0,0 +1,121 @@ +// 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.After +import org.junit.Assert +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() + } + + @After + fun tearDown() { + // Clear the static map after each test + NumberMatchHelper.numberMatchMap.clear() + } + + @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.assertTrue(storedValue == mockNumberMatchValue) + } + + @Test + fun `test postMessageToBroker with empty json`() { + // Call the method + authUxJavaScriptInterface.postMessageToBroker("{}") + + // Should not get an exception + } + + @Test + fun `test postMessageToBroker with non-json string`() { + // Call the method + authUxJavaScriptInterface.postMessageToBroker("NotAJson") + + // Should not get an exception + } + + @Test + fun `test isValidUrlForInterface with valid AAD URL`() { + val validUrl = "https://login.microsoftonline.com/common/oauth2/authorize" + Assert.assertTrue(AuthUxJavaScriptInterface.isValidUrlForInterface(validUrl)) + } + + @Test + fun `test isValidUrlForInterface with valid MSA URL`() { + val validUrl = "https://login.live.com/oauth20_authorize.srf" + Assert.assertTrue(AuthUxJavaScriptInterface.isValidUrlForInterface(validUrl)) + } + + @Test + fun `test isValidUrlForInterface with null URL`() { + val nullUrl: String? = null + Assert.assertFalse(AuthUxJavaScriptInterface.isValidUrlForInterface(nullUrl)) + } + + @Test + fun `test isValidUrlForInterface with invalid URL`() { + val invalidUrl = "https://example.com" + Assert.assertFalse(AuthUxJavaScriptInterface.isValidUrlForInterface(invalidUrl)) + } + + @Test + fun `test isValidUrlForInterface with empty URL`() { + val emptyUrl = "" + Assert.assertFalse(AuthUxJavaScriptInterface.isValidUrlForInterface(emptyUrl)) + } +} diff --git a/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayloadTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayloadTest.kt new file mode 100644 index 0000000000..951e60c740 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/internal/broker/AuthUxJsonPayloadTest.kt @@ -0,0 +1,114 @@ +// 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.google.gson.GsonBuilder +import com.google.gson.JsonParseException +import org.junit.Assert.* +import org.junit.Test + +class AuthUxJsonPayloadTest { + + private val gson = GsonBuilder() + .registerTypeAdapter(AuthUxJsonPayload::class.java, AuthUxJsonPayloadKTDeserializer()) + .create() + + @Test + fun `test deserialization of valid JSON`() { + val json = """ + { + "correlationID": "12345", + "action_name": "write_data", + "action_component": "broker", + "params": { + "function": "NUMBER_MATCH", + "data": { + "sessionID": "67890", + "numberMatch": "123456" + } + } + } + """.trimIndent() + + val payload = gson.fromJson(json, AuthUxJsonPayload::class.java) + + assertNotNull(payload) + assertEquals("12345", payload.correlationId) + assertEquals("write_data", payload.actionName) + assertEquals("broker", payload.actionComponent) + + val params = payload.params + assertNotNull(params) + assertEquals("NUMBER_MATCH", params?.function) + + val data = params?.data + assertNotNull(data) + assertEquals("67890", data?.sessionId) + assertEquals("123456", data?.numberMatch) + } + + @Test + fun `test deserialization of JSON with missing optional fields`() { + val json = """ + { + "correlationID": "12345", + "action_name": "write_data", + "action_component": "broker", + "params" : { + "invalidField": "invalidField" + } + } + """.trimIndent() + + val payload = gson.fromJson(json, AuthUxJsonPayload::class.java) + + assertNotNull(payload) + assertEquals("12345", payload.correlationId) + assertEquals("write_data", payload.actionName) + assertEquals("broker", payload.actionComponent) + assertNotNull(payload.params) + assertNull(payload.params?.data) + assertNull(payload.params?.function) + } + + @Test(expected = JsonParseException::class) + fun `test deserialization of JSON with missing mandatory fields, exception expected`() { + val json = """ + { + "correlationID": "12345", + "action_name": "write_data" + } + """.trimIndent() + + // This should throw an exception because "action_component" and "params" is missing + gson.fromJson(json, AuthUxJsonPayload::class.java) + + } + + @Test(expected = JsonParseException::class) + fun `test deserialization of empty JSON, exception expected`() { + val json = "{}" + + // This should throw an exception because the JSON is empty + gson.fromJson(json, AuthUxJsonPayload::class.java) + } +} diff --git a/common/src/test/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelperTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelperTest.kt new file mode 100644 index 0000000000..6c8a264d24 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/internal/numberMatch/NumberMatchHelperTest.kt @@ -0,0 +1,84 @@ +// 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 org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class NumberMatchHelperTest { + + private val mockSessionId = "1234" + private val mockNumberMatchValue = "00" + + @Before + fun setUp() { + // Clear the map before each test to ensure a clean state + NumberMatchHelper.clearNumberMatchMap() + } + + @Test + fun `test storeNumberMatch with valid inputs`() { + // Store a valid session ID and number match + val sessionId = mockSessionId + val numberMatch = mockNumberMatchValue + NumberMatchHelper.storeNumberMatch(sessionId, numberMatch) + + // Verify that the map contains the correct value + assertEquals(mockNumberMatchValue, NumberMatchHelper.numberMatchMap[sessionId]) + } + + @Test + fun `test storeNumberMatch with null sessionId`() { + // Attempt to store a null session ID + val numberMatch = mockNumberMatchValue + NumberMatchHelper.storeNumberMatch(null, numberMatch) + + // Verify that the map is still empty + assertTrue(NumberMatchHelper.numberMatchMap.isEmpty()) + } + + @Test + fun `test storeNumberMatch with null numberMatch`() { + // Attempt to store a null number match + val sessionId = mockSessionId + NumberMatchHelper.storeNumberMatch(sessionId, null) + + // Verify that the map is still empty + assertTrue(NumberMatchHelper.numberMatchMap.isEmpty()) + } + + @Test + fun `test clearNumberMatchMap`() { + // Add an entry to the map + val sessionId = mockSessionId + val numberMatch = mockNumberMatchValue + NumberMatchHelper.storeNumberMatch(sessionId, numberMatch) + + // Clear the map + NumberMatchHelper.clearNumberMatchMap() + + // Verify that the map is empty + assertTrue(NumberMatchHelper.numberMatchMap.isEmpty()) + } +}