Skip to content

Commit 3a20f4a

Browse files
p3dr0rvCopilot
andauthored
Add WebAuthn version support in configuration, Fixes AB#3385532 (#2393)
[AB#3385532](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3385532) https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview/pullrequest/20357 ### Add WebAuthn Version Support and Passkey Headers This PR adds support for handling the WebAuthn protocol version in the app configuration and authentication flow for broker-less scenarios. also enables testing on WEBVIEW PPE MSA **Changes:** - Added a new `webauthn_version` field to `PublicClientApplicationConfiguration`, including serialization, accessors, and merge logic, allowing apps to define and retrieve the WebAuthn version from configuration files. - Updated `CommandParametersAdapter` to include passkey protocol headers in authentication requests when WebAuthn is enabled, supported (Android 9+), Authorization agent is Webview and the version is 1.1. - Updated the test app (`MsalWrapper`) to append the `msaoauth2=true` parameter to query strings when running in the pre-production environment with WebAuthn 1.1 enabled, enabling proper testing of WebAuthn flows. Related PR: AzureAD/microsoft-authentication-library-common-for-android#2769 Test 1- create account https://signup.live-int.com/?lic=1 2 - Install msal test app, (ensure no broker is installed) 3 - change config to MSA_WEBVIEW_PPE 4- Click acquire token and complete auth flow (username, password) 5 - User is presented with the option to register a passkey, complete the flow, and you will end up with a token and a passkey. 6 - try again with no user selected and use the passkey. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 71ac706 commit 3a20f4a

File tree

11 files changed

+235
-3
lines changed

11 files changed

+235
-3
lines changed

changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr
22

33
vNext
44
----------
5+
- [MINOR] Add WebAuthN version support in configuration (#2393)
56

67
Version 8.0.2
78
----------

common

Submodule common updated 30 files

gradle/versions.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ ext {
8080
AndroidCredentialsVersion="1.2.2"
8181
LegacyFidoApiVersion="20.1.0"
8282
GoogleIdVersion="1.1.0"
83+
webkitVersion="1.14.0"
8384

8485
// microsoft-diagnostics-uploader app versions
8586
powerliftVersion = "0.14.7"

msal/src/main/java/com/microsoft/identity/client/PublicClientApplicationConfiguration.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.TELEMETRY;
4242
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.USE_BROKER;
4343
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEBAUTHN_CAPABLE;
44+
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEBAUTHN_VERSION;
4445
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEB_VIEW_ZOOM_CONTROLS_ENABLED;
4546
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEB_VIEW_ZOOM_ENABLED;
4647
import static com.microsoft.identity.client.exception.MsalClientException.APP_MANIFEST_VALIDATION_ERROR;
@@ -114,6 +115,8 @@ public static final class SerializedNames {
114115
static final String HANDLE_TASKS_WITH_NULL_TASKAFFINITY = "handle_null_taskaffinity";
115116
static final String AUTHORIZATION_IN_CURRENT_TASK = "authorization_in_current_task";
116117
static final String WEBAUTHN_CAPABLE = "webauthn_capable";
118+
static final String WEBAUTHN_VERSION = "webauthn_version";
119+
117120
}
118121

119122
@SerializedName(CLIENT_ID)
@@ -187,6 +190,10 @@ public static final class SerializedNames {
187190
@SerializedName(WEBAUTHN_CAPABLE)
188191
private Boolean webauthnCapable;
189192

193+
194+
@SerializedName(WEBAUTHN_VERSION)
195+
private String webauthnVersion;
196+
190197
transient private OAuth2TokenCache mOAuth2TokenCache;
191198

192199
transient private Context mAppContext;
@@ -430,6 +437,10 @@ public Boolean isWebauthnCapable() {
430437
return Boolean.TRUE.equals(webauthnCapable);
431438
}
432439

440+
public String getWebauthnVersion() {
441+
return webauthnVersion;
442+
}
443+
433444
public Authority getDefaultAuthority() {
434445
if (mAuthorities != null) {
435446
if (mAuthorities.size() > 1) {
@@ -515,6 +526,8 @@ public void mergeConfiguration(PublicClientApplicationConfiguration config) {
515526
this.handleNullTaskAffinity = config.handleNullTaskAffinity == null ? this.handleNullTaskAffinity : config.handleNullTaskAffinity;
516527
this.isAuthorizationInCurrentTask = config.isAuthorizationInCurrentTask == null ? this.isAuthorizationInCurrentTask : config.isAuthorizationInCurrentTask;
517528
this.webauthnCapable = config.webauthnCapable == null ? this.webauthnCapable : config.webauthnCapable;
529+
this.webauthnVersion = config.webauthnVersion == null ? this.webauthnVersion : config.webauthnVersion;
530+
518531
}
519532

520533
public void validateConfiguration() {

msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import android.content.Context;
2626
import android.content.pm.PackageInfo;
2727
import android.content.pm.PackageManager;
28+
import android.os.Build;
2829

2930
import com.microsoft.identity.client.AcquireTokenParameters;
3031
import com.microsoft.identity.client.AcquireTokenSilentParameters;
@@ -33,6 +34,7 @@
3334
import com.microsoft.identity.client.ITenantProfile;
3435
import com.microsoft.identity.client.MultiTenantAccount;
3536
import com.microsoft.identity.common.internal.platform.AndroidPlatformUtil;
37+
import com.microsoft.identity.common.java.constants.FidoConstants;
3638
import com.microsoft.identity.common.java.logging.DiagnosticContext;
3739
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITChallengeAuthMethodCommandParameters;
3840
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITContinueCommandParameters;
@@ -86,6 +88,7 @@
8688

8789
import java.util.ArrayList;
8890
import java.util.Arrays;
91+
import java.util.HashMap;
8992
import java.util.HashSet;
9093
import java.util.List;
9194
import java.util.Map;
@@ -204,6 +207,7 @@ public static InteractiveTokenCommandParameters createInteractiveTokenCommandPar
204207
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
205208
.correlationId(parameters.getCorrelationId())
206209
.preferredAuthMethod(parameters.getPreferredAuthMethod())
210+
.requestHeaders(addPasskeyHeader(parameters.getExtraQueryStringParameters(), configuration))
207211
.build();
208212
}
209213

@@ -1371,4 +1375,81 @@ public static List<Map.Entry<String, String>> appendToExtraQueryParametersIfWebA
13711375
ArrayList<Map.Entry<String, String>> result = queryStringParameters != null ? new ArrayList<>(queryStringParameters) : new ArrayList<>();
13721376
return AndroidPlatformUtil.updateWithOrDeleteWebAuthnParam(result, configuration.isWebauthnCapable());
13731377
}
1378+
1379+
1380+
/**
1381+
* Adds passkey protocol headers if WebAuthn is enabled and supported (Android 9+, version 1.1).
1382+
*
1383+
* @param queryStringParameters Query parameters from the authentication request.
1384+
* @param configuration Application configuration with WebAuthn settings.
1385+
* @return HashMap with passkey headers if conditions are met, otherwise empty.
1386+
*/
1387+
@NonNull
1388+
private static HashMap<String, String> addPasskeyHeader(
1389+
@Nullable final List<Map.Entry<String, String>> queryStringParameters,
1390+
@NonNull final PublicClientApplicationConfiguration configuration) {
1391+
1392+
final String methodTag = TAG + ":addPasskeyHeader";
1393+
final HashMap<String, String> headers = new HashMap<>();
1394+
1395+
// Passkey functionality requires Android 9 (Pie) or higher
1396+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
1397+
return headers;
1398+
}
1399+
1400+
// Skip if not using WebView authorization agent
1401+
if (!AuthorizationAgent.WEBVIEW.equals(configuration.getAuthorizationAgent())) {
1402+
return headers;
1403+
}
1404+
1405+
1406+
// Skip if no webauthn query parameter and the configuration isn't webauthn-capable
1407+
if (!containsValidWebAuth(queryStringParameters) && !configuration.isWebauthnCapable()) {
1408+
return headers;
1409+
}
1410+
1411+
if (configuration.getWebauthnVersion() == null) {
1412+
return headers;
1413+
}
1414+
1415+
switch (configuration.getWebauthnVersion()) {
1416+
case FidoConstants.PASSKEY_PROTOCOL_VERSION_1_0:
1417+
headers.put(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME, FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_ONLY);
1418+
Logger.verbose(methodTag, "Passkey header added for WebAuthn version 1.0");
1419+
break;
1420+
case FidoConstants.PASSKEY_PROTOCOL_VERSION_1_1:
1421+
headers.put(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME, FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_AND_REG);
1422+
Logger.verbose(methodTag, "Passkey header added for WebAuthn version 1.1");
1423+
break;
1424+
default:
1425+
Logger.verbose(methodTag, "Unsupported WebAuthn version: " + configuration.getWebauthnVersion());
1426+
break;
1427+
}
1428+
1429+
return headers;
1430+
}
1431+
1432+
/**
1433+
* Determines whether the given list of query parameters contains a valid WebAuthn entry.
1434+
*
1435+
* @param queryParameters the list of query parameters to inspect, may be null.
1436+
* @return {@code true} if a parameter with both the expected WebAuthn key and value is found; {@code false} otherwise.
1437+
*/
1438+
private static boolean containsValidWebAuth(
1439+
@Nullable final List<Map.Entry<String, String>> queryParameters) {
1440+
1441+
if (queryParameters == null || queryParameters.isEmpty()) {
1442+
return false;
1443+
}
1444+
1445+
for (Map.Entry<String, String> entry : queryParameters) {
1446+
if (FidoConstants.WEBAUTHN_QUERY_PARAMETER_FIELD.equals(entry.getKey())
1447+
&& FidoConstants.WEBAUTHN_QUERY_PARAMETER_VALUE.equals(entry.getValue())) {
1448+
return true;
1449+
}
1450+
}
1451+
1452+
return false;
1453+
}
1454+
13741455
}

msal/src/test/java/com/microsoft/identity/client/CommandParametersTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInWithContinuationTokenCommandParameters;
4848
import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache;
4949
import com.microsoft.identity.common.java.exception.ClientException;
50+
import com.microsoft.identity.common.java.ui.AuthorizationAgent;
5051
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
5152
import com.microsoft.identity.msal.R;
5253
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration;
@@ -365,6 +366,103 @@ public void testAppendToExtraQueryParametersIfWebAuthnCapable_WebAuthnCapableFal
365366
Assert.assertEquals(combinedQueryParameters.size(), 1);
366367
}
367368

369+
370+
@Test
371+
@Config(sdk=28)
372+
public void testPasskeyHeader_AddedWhenWebAuthnConfigurationEnabled() throws ClientException {
373+
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
374+
.createInteractiveTokenCommandParameters(
375+
getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE),
376+
getCache(),
377+
getAcquireTokenParametersWithClaims()
378+
);
379+
Assert.assertTrue(commandParameters
380+
.getRequestHeaders()
381+
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
382+
);
383+
Assert.assertEquals(
384+
FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_AND_REG,
385+
commandParameters.getRequestHeaders().get(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
386+
);
387+
}
388+
389+
@Test
390+
@Config(sdk=28)
391+
public void testPasskeyHeader_NotAddedWhenAuthorizationAgentIsDefault() throws ClientException {
392+
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
393+
Mockito.when(mockConfig.getAuthorizationAgent()).thenReturn(AuthorizationAgent.DEFAULT);
394+
395+
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
396+
.createInteractiveTokenCommandParameters(
397+
mockConfig,
398+
getCache(),
399+
getAcquireTokenParametersWithClaims()
400+
);
401+
Assert.assertFalse(commandParameters
402+
.getRequestHeaders()
403+
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
404+
);
405+
}
406+
407+
@Test
408+
@Config(sdk=28)
409+
public void testPasskeyHeader_NotAddedWhenWebAuthnNotCapable() throws ClientException {
410+
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
411+
Mockito.when(mockConfig.isWebauthnCapable()).thenReturn(false);
412+
413+
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
414+
.createInteractiveTokenCommandParameters(
415+
mockConfig,
416+
getCache(),
417+
getAcquireTokenParametersWithClaims()
418+
);
419+
Assert.assertFalse(commandParameters
420+
.getRequestHeaders()
421+
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
422+
);
423+
}
424+
425+
@Test
426+
@Config(sdk=28)
427+
public void testPasskeyHeader_NotAddedWhenWebAuthnVersionUnsupported() throws ClientException {
428+
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
429+
Mockito.when(mockConfig.getWebauthnVersion()).thenReturn("3.0");
430+
431+
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
432+
.createInteractiveTokenCommandParameters(
433+
mockConfig,
434+
getCache(),
435+
getAcquireTokenParametersWithClaims()
436+
);
437+
Assert.assertFalse(commandParameters
438+
.getRequestHeaders()
439+
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
440+
);
441+
}
442+
443+
@Test
444+
@Config(sdk=28)
445+
public void testPasskeyHeader_NotAddedWhenWebAuthnVersion1_0() throws ClientException {
446+
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
447+
Mockito.when(mockConfig.getWebauthnVersion()).thenReturn("1.0");
448+
449+
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
450+
.createInteractiveTokenCommandParameters(
451+
mockConfig,
452+
getCache(),
453+
getAcquireTokenParametersWithClaims()
454+
);
455+
Assert.assertTrue(commandParameters
456+
.getRequestHeaders()
457+
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
458+
);
459+
Assert.assertEquals(
460+
FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_ONLY,
461+
commandParameters.getRequestHeaders().get(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
462+
);
463+
}
464+
465+
368466
@Test
369467
public void testCreateSignInStartCommandParameters_CommandParamsContainsExpectedParams() throws ClientException {
370468
List<String> challengeTypes = new ArrayList<>(Collections.singletonList("OOB"));

msal/src/test/res/raw/webauthn_capable.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"client_id" : "4b0db8c2-9f26-4417-8bde-3f0e3656f8e0",
3-
"authorization_user_agent" : "DEFAULT",
3+
"authorization_user_agent" : "WEBVIEW",
44
"redirect_uri" : "msauth://com.microsoft.identity.client.sample.local/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
55
"multiple_clouds_supported":true,
66
"broker_redirect_uri_registered": true,
77
"account_mode": "MULTIPLE",
88
"webauthn_capable": true,
9+
"webauthn_version": 1.1,
910
"authorities" : [
1011
{
1112
"type": "AAD",

testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/Constants.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ enum ConfigFile {
5656
WEBVIEW_WITH_PPE,
5757

5858
WEBVIEW_PPE_MSA,
59-
ONEBOX
59+
ONEBOX,
60+
61+
WEBVIEW_MSA_PASSKEY_REG
6062
}
6163

6264
public static int getResourceIdFromConfigFile(ConfigFile configFile) {
@@ -124,6 +126,10 @@ public static int getResourceIdFromConfigFile(ConfigFile configFile) {
124126

125127
case ONEBOX:
126128
return R.raw.msal_config_onebox;
129+
130+
case WEBVIEW_MSA_PASSKEY_REG:
131+
return R.raw.msal_config_webview_msa_passkey_reg;
132+
127133
}
128134

129135
return R.raw.msal_config_default;

testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/MsalWrapper.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@
2626
import com.microsoft.identity.client.exception.MsalServiceException;
2727
import com.microsoft.identity.client.exception.MsalUiRequiredException;
2828
import com.microsoft.identity.common.internal.ui.browser.AndroidBrowserSelector;
29+
import com.microsoft.identity.common.java.authorities.Environment;
2930
import com.microsoft.identity.common.java.browser.Browser;
31+
import com.microsoft.identity.common.java.constants.FidoConstants;
3032
import com.microsoft.identity.common.java.exception.BaseException;
33+
import com.microsoft.identity.common.java.ui.AuthorizationAgent;
3134
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
3235
import com.microsoft.identity.common.java.util.StringUtil;
3336

@@ -167,6 +170,18 @@ private AcquireTokenParameters.Builder getAcquireTokenParametersBuilder(@NonNull
167170
if (requestOptions.isAllowSignInFromOtherDevice()) {
168171
extraQP.add(new AbstractMap.SimpleEntry<>("is_remote_login_allowed", Boolean.toString(true)));
169172
}
173+
174+
// Add "msaoauth2=true" to test WebAuthN on WebView PPE
175+
final String webauthnVersion = getApp().getConfiguration().getWebauthnVersion();
176+
final Environment environment = getApp().getConfiguration().getEnvironment();
177+
final AuthorizationAgent authorizationAgent = getApp().getConfiguration().getAuthorizationAgent();
178+
if (getApp().getConfiguration().isWebauthnCapable()
179+
&& FidoConstants.PASSKEY_PROTOCOL_VERSION_1_1.equals(webauthnVersion)
180+
&& Environment.PreProduction.equals(environment)
181+
&& AuthorizationAgent.WEBVIEW.equals(authorizationAgent)) {
182+
extraQP.add(new AbstractMap.SimpleEntry<>("msaoauth2", Boolean.toString(true)));
183+
}
184+
170185
builder.withAuthorizationQueryStringParameters(extraQP);
171186

172187
if (!StringUtil.isNullOrEmpty(requestOptions.getAuthority())) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"client_id" : "9668f2bd-6103-4292-9024-84fa2d1b6fb2",
3+
"authorization_user_agent" : "WEBVIEW",
4+
"redirect_uri" : "msauth://com.msft.identity.client.sample.local/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
5+
"webauthn_capable" : true,
6+
"webauthn_version" : 1.1,
7+
"authorities" : [
8+
{
9+
"type": "AAD",
10+
"audience": {
11+
"type": "AzureADandPersonalMicrosoftAccount"
12+
}
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)