Skip to content

Commit c8acef3

Browse files
authored
Expose a JavaScript API in brokered Webviews to facilitate Improved Same Device NumberMatch , Fixes AB#3203956 (#2617)
SPEC: https://microsoft-my.sharepoint-df.com/:w:/r/personal/siddhijain_microsoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BD1D944D5-2047-40AB-B8F9-98506BF212A3%7D&file=Engineering%20design%20-%20Number%20matching%20on%20same%20device.docx&action=default&mobileredirect=true&share=IQHVRNnRRyCrQLj5mFBr8hKjAbj81fEnpO6X-99laqs2j_M&wdOrigin=TEAMS-MAGLEV.p2p_ns.rwc&wdExp=TEAMS-TREATMENT&wdhostclicktime=1743094076241&web=1 Word Doc: for JavaScript Api: https://microsoft-my.sharepoint-df.com/:w:/p/veenasoman/EY1AZIeT8X5KrXVz97Vx520B3Jj0fBLSPlklnoRvcmbh0Q?e=ZVVUrw&nav=eyJoIjoiMjEzMzE1Mzg5NSJ9 Structure has changed a bit for this. To facilitate future work, we will have a generalized JavaScript API that takes in a json string payload. This is used to parse out a function name, and data field, both of which are used to call a specific function in broker code. This same functionality will be used next month for CA Block improvment work (I don't have a spec to this one yet). Expected method call in JavaScript is now something like this, we are working on finalizing json schema: `BrokerJS.postToBroker('{function: NUMBER_MATCH,data: {sessionID: id, numberMatch: number}}')` I added some unit tests in the broker PR, but primary validation will be when ests exposes a test slice that calls the JavaScript API. Did some testing in our webview class to call javascript code, and was able to prompt the numberMatch method. Broker PR: AzureAD/ad-accounts-for-android#3073 [AB#3203956](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3203956)
1 parent a5670a3 commit c8acef3

10 files changed

Lines changed: 725 additions & 10 deletions

File tree

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] Expose a JavaScript API in brokered Webviews to facilitate Improved Same Device NumberMatch (#2617)
34
- [MINOR] Add API for resource account provisioning (API only) (#2640)
45

56
Version 21.1.0

common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,16 @@ public static String computeMaxHostBrokerProtocol() {
12221222
*/
12231223
public static final String REDIRECT_PREFIX = "msauth";
12241224

1225+
/**
1226+
* Prefix for AAD urls
1227+
*/
1228+
public static final String AAD_URL_HOST_PREFIX = "login.microsoftonline.";
1229+
1230+
/**
1231+
* Prefix for MSA urls
1232+
*/
1233+
public static final String MSA_URL_HOST_PREFIX = "login.live.";
1234+
12251235
/**
12261236
* Encoded delimiter for redirect.
12271237
*/
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.broker
24+
25+
import android.webkit.JavascriptInterface
26+
import com.google.gson.GsonBuilder
27+
import com.google.gson.JsonParseException
28+
import com.google.gson.JsonSyntaxException
29+
import com.google.gson.stream.MalformedJsonException
30+
import com.microsoft.identity.common.adal.internal.AuthenticationConstants
31+
import com.microsoft.identity.common.internal.numberMatch.NumberMatchHelper
32+
import com.microsoft.identity.common.logging.Logger
33+
import java.net.MalformedURLException
34+
import java.net.URL
35+
36+
/**
37+
* JavaScript API to receive JSON string payloads from AuthUX in order to facilitate calling various
38+
* broker methods.
39+
*/
40+
class AuthUxJavaScriptInterface {
41+
42+
// Store number matches in a static hash map
43+
// No need to persist this storage beyond the current broker process, but we need to keep them
44+
// long enough for AuthApp to call the broker api to fetch the number match
45+
companion object {
46+
val TAG = AuthUxJavaScriptInterface::class.java.simpleName
47+
private const val JAVASCRIPT_INTERFACE_NAME = "ClientBrokerJS"
48+
49+
fun getInterfaceName() : String {
50+
return JAVASCRIPT_INTERFACE_NAME
51+
}
52+
53+
/**
54+
* Helper method to determine if url is a valid Url for the JS Interface
55+
* @param url url being loaded
56+
* @return true if url is a valid, safe url, false otherwise
57+
*/
58+
fun isValidUrlForInterface(urlString: String?): Boolean {
59+
// If url is null, return false
60+
if (urlString.isNullOrEmpty()) {
61+
return false
62+
}
63+
64+
val url : URL
65+
try {
66+
url = URL(urlString)
67+
} catch (e: MalformedURLException) {
68+
// If url is not a valid URL, return false
69+
Logger.warn(TAG, "Malformed URL passed.")
70+
return false
71+
72+
}
73+
74+
val host = url.host
75+
76+
// Otherwise, make sure url is a valid url
77+
// We only want to allow URLs that have the AAD or MSA url hosts
78+
return host.startsWith(AuthenticationConstants.Broker.AAD_URL_HOST_PREFIX) ||
79+
host.startsWith(AuthenticationConstants.Broker.MSA_URL_HOST_PREFIX)
80+
}
81+
}
82+
83+
/**
84+
* Method to receive a JSON string payload from AuthUX through JavaScript API.
85+
* Schema for the Json Payload:
86+
* {
87+
* "correlationID": "SOME_CORRELATION_ID" ,
88+
* "action_name":"write_data",
89+
* "action_component":"broker",
90+
* "params":
91+
* {
92+
* "function": "NUMBER_MATCH",
93+
* "data":
94+
* {
95+
* "sessionID": "$mockSessionId",
96+
* "numberMatch": "$mockNumberMatchValue"
97+
* }
98+
* }
99+
* }
100+
* TODO: This is currently the schema set for numberMatch, there may be some additions made for
101+
* the more generalized JSON Schema for future Server-side to broker communication through JS.
102+
*
103+
* 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
104+
*/
105+
@JavascriptInterface
106+
fun postMessageToBroker(jsonPayload: String) {
107+
val methodTag = "$TAG:postMessageToBroker"
108+
Logger.info(methodTag, "Received a payload from AuthUX through JavaScript API.")
109+
110+
try {
111+
val payloadObject = parseJsonToAuthUxJsonPayloadObject(jsonPayload)
112+
113+
Logger.info(methodTag, "Correlation ID during JavaScript Call: [${payloadObject.correlationId}]")
114+
115+
116+
// TODO: Leaving these here, as these will be relevant for next WebCP feature
117+
// val actionName = payloadObject.actionName
118+
// val actionComponent = payloadObject.actionComponent
119+
120+
val parameters = payloadObject.params
121+
if (parameters == null) {
122+
Logger.warn(methodTag, "Payload from AuthUX contained no \"params\" field.")
123+
return
124+
}
125+
126+
val function = parameters.function
127+
128+
Logger.info(methodTag, "Function name: [$function]")
129+
130+
val data = parameters.data
131+
if (data == null) {
132+
Logger.warn(methodTag, "Payload from AuthUX contained no \"data\" field.")
133+
return
134+
}
135+
136+
when (function) {
137+
FunctionNames.NUMBER_MATCH.name ->
138+
NumberMatchHelper.storeNumberMatch(
139+
data.sessionId,
140+
data.numberMatch)
141+
else ->
142+
Logger.warn(methodTag, "Payload from AuthUX contained an unknown function name.")
143+
}
144+
} catch (e: Exception) { // If we run into exceptions, we don't want to kill the broker
145+
when (e) {
146+
is NullPointerException -> {
147+
Logger.error(methodTag, "Payload with missing mandatory fields sent through JavaScriptInterface", e)
148+
}
149+
is MalformedJsonException, is JsonSyntaxException, is JsonParseException -> {
150+
Logger.error(methodTag, "Error Parsing JSON payload sent through JavaScriptInterface", e)
151+
}
152+
else -> {
153+
Logger.error(methodTag, "Unknown error occurred while processing the payload.", e)
154+
}
155+
}
156+
}
157+
}
158+
159+
private fun parseJsonToAuthUxJsonPayloadObject(jsonString: String): AuthUxJsonPayload{
160+
val gson = GsonBuilder()
161+
.registerTypeAdapter(AuthUxJsonPayload::class.java, AuthUxJsonPayloadKTDeserializer())
162+
.create()
163+
return gson.fromJson(jsonString, AuthUxJsonPayload::class.java)
164+
}
165+
166+
/**
167+
* Enum class to hold function names
168+
*/
169+
enum class FunctionNames {
170+
NUMBER_MATCH
171+
}
172+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.broker
24+
25+
import com.google.gson.JsonDeserializationContext
26+
import com.google.gson.JsonDeserializer
27+
import com.google.gson.JsonElement
28+
import com.google.gson.JsonParseException
29+
import com.google.gson.annotations.SerializedName
30+
import java.lang.reflect.Type
31+
32+
/**
33+
* Data class representing the JSON payload object received from AuthUX.
34+
*
35+
* @property correlationId The correlation ID for the request.
36+
* @property actionName The name of the action being performed.
37+
* @property actionComponent The component responsible for the action.
38+
* @property params The parameters for the action, including function and data.
39+
*/
40+
data class AuthUxJsonPayload(
41+
val correlationId: String,
42+
val actionName: String,
43+
val actionComponent: String,
44+
val params: AuthUxParams?
45+
)
46+
47+
/**
48+
* Data class representing the parameters for the action, including function and data.
49+
*
50+
* @property function The function to be executed.
51+
* @property data The data associated with the function.
52+
*/
53+
data class AuthUxParams(
54+
@SerializedName(SerializedNames.FUNCTION)
55+
val function: String?,
56+
57+
@SerializedName(SerializedNames.DATA)
58+
val data: AuthUxData?
59+
)
60+
61+
/**
62+
* Data class representing the data associated with the JS API call.
63+
*
64+
* @property sessionId The session ID for the request.
65+
* @property numberMatch The number match value.
66+
*/
67+
data class AuthUxData(
68+
@SerializedName(SerializedNames.SESSION_ID)
69+
val sessionId: String?,
70+
71+
@SerializedName(SerializedNames.NUMBER_MATCH)
72+
val numberMatch: String?
73+
)
74+
75+
class AuthUxJsonPayloadKTDeserializer : JsonDeserializer<AuthUxJsonPayload> {
76+
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): AuthUxJsonPayload {
77+
val jsonObject = json.asJsonObject
78+
79+
// Validate required fields
80+
val correlationId = jsonObject.get(SerializedNames.CORRELATION_ID)?.asString
81+
?: throw JsonParseException("correlationID is required and cannot be null")
82+
val actionName = jsonObject.get(SerializedNames.ACTION_NAME)?.asString
83+
?: throw JsonParseException("action_name is required and cannot be null")
84+
val actionComponent = jsonObject.get(SerializedNames.ACTION_COMPONENT)?.asString
85+
?: throw JsonParseException("action_component is required and cannot be null")
86+
87+
// Deserialize params if present
88+
val params = jsonObject.get("params")?.let {
89+
context.deserialize<AuthUxParams>(it, AuthUxParams::class.java)
90+
}
91+
92+
return AuthUxJsonPayload(
93+
correlationId = correlationId,
94+
actionName = actionName,
95+
actionComponent = actionComponent,
96+
params = params
97+
)
98+
}
99+
}
100+
101+
object SerializedNames {
102+
const val CORRELATION_ID = "correlationID"
103+
const val ACTION_NAME = "action_name"
104+
const val ACTION_COMPONENT = "action_component"
105+
const val PARAMS = "params"
106+
const val FUNCTION = "function"
107+
const val DATA = "data"
108+
const val SESSION_ID = "sessionID"
109+
const val NUMBER_MATCH = "numberMatch"
110+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.numberMatch
24+
25+
import com.microsoft.identity.common.logging.Logger
26+
27+
/**
28+
* Helper to facilitate NumberMatchFlow. Used in conjunction with {@link AuthUxJavaScriptInterface}
29+
* When authenticator is installed, and phone uses MFA or PSI in an interactive flow, a number
30+
* matching challenge is issued, where used is given a number and asked to open authenticator and check
31+
* for the same number in authenticator UI. This feature cuts out one UI step, where this API is used to
32+
* supply the number match value and store it in ephemeral storage (kept as long as current broker
33+
* process is alive), where Authenticator can call a broker API to fetch the number match, and immediately
34+
* prompt user for consent, rather than first asking them to check the number match.
35+
*/
36+
class NumberMatchHelper {
37+
38+
// Store number matches in a static hash map
39+
// No need to persist this storage beyond the current broker process, but we need to keep them
40+
// long enough for AuthApp to call the broker api to fetch the number match
41+
companion object {
42+
val TAG = NumberMatchHelper::class.java.simpleName
43+
val numberMatchMap: HashMap<String, String> = HashMap()
44+
const val SESSION_ID_ATTRIBUTE_NAME = "sessionID"
45+
const val NUMBER_MATCH_ATTRIBUTE_NAME = "numberMatch"
46+
47+
/**
48+
* Method to add a key:value pair of sessionID:numberMatch to static hashmap. This hashmap will be accessed
49+
* by broker api to get the number match for a particular sessionID.
50+
*/
51+
fun storeNumberMatch(sessionId: String?, numberMatch: String?) {
52+
val methodTag = "$TAG:storeNumberMatch"
53+
Logger.info(methodTag,
54+
"Adding entry in NumberMatch hashmap for session ID: $sessionId")
55+
56+
// If both parameters are non-null, add a new entry to the hashmap
57+
if (sessionId != null && numberMatch != null) {
58+
numberMatchMap[sessionId] = numberMatch
59+
}
60+
// If either parameter is null, do nothing
61+
else {
62+
Logger.warn(methodTag,
63+
"Either session ID or number match is null. Nothing to add for number match."
64+
)
65+
}
66+
}
67+
68+
/**
69+
* Clear existing number match key:value pairs
70+
*/
71+
fun clearNumberMatchMap() {
72+
numberMatchMap.clear()
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)