Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr

vNext
----------
- [MINOR] Add WebAuthN version support in configuration (#2393)

Version 8.0.2
----------
Expand Down
2 changes: 1 addition & 1 deletion common
Submodule common updated 25 files
+684 −0 .github/copilot-instructions.md
+5 −2 changelog.txt
+2 −1 common/build.gradle
+205 −0 common/src/main/assets/js-bridge.js
+20 −0 common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java
+113 −0 common/src/main/java/com/microsoft/identity/common/internal/controllers/BrokerMsalController.java
+6 −3 common/src/main/java/com/microsoft/identity/common/internal/fido/FidoChallengeField.kt
+81 −0 common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/CredentialManagerHandler.kt
+228 −0 common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyReplyChannel.kt
+383 −0 common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt
+99 −15 common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java
+32 −0 common/src/main/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapter.java
+38 −1 common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java
+34 −1 common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java
+58 −0 common/src/main/java/com/microsoft/identity/common/internal/ui/webview/JsScriptRecord.kt
+21 −0 common/src/main/java/com/microsoft/identity/common/internal/ui/webview/OAuth2WebViewClient.java
+291 −0 common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyReplyChannelTest.kt
+558 −0 common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListenerTest.kt
+11 −0 .../src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java
+8 −7 common4j/src/main/com/microsoft/identity/common/java/cache/MsalOAuth2TokenCache.java
+26 −4 common4j/src/main/com/microsoft/identity/common/java/constants/FidoConstants.kt
+5 −0 common4j/src/main/com/microsoft/identity/common/java/exception/ErrorStrings.java
+2 −3 common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java
+3 −1 common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java
+1 −0 gradle/versions.gradle
1 change: 1 addition & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ ext {
AndroidCredentialsVersion="1.2.2"
LegacyFidoApiVersion="20.1.0"
GoogleIdVersion="1.1.0"
webkitVersion="1.14.0"

// microsoft-diagnostics-uploader app versions
powerliftVersion = "0.14.7"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.TELEMETRY;
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.USE_BROKER;
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEBAUTHN_CAPABLE;
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEBAUTHN_VERSION;
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEB_VIEW_ZOOM_CONTROLS_ENABLED;
import static com.microsoft.identity.client.PublicClientApplicationConfiguration.SerializedNames.WEB_VIEW_ZOOM_ENABLED;
import static com.microsoft.identity.client.exception.MsalClientException.APP_MANIFEST_VALIDATION_ERROR;
Expand Down Expand Up @@ -114,6 +115,8 @@ public static final class SerializedNames {
static final String HANDLE_TASKS_WITH_NULL_TASKAFFINITY = "handle_null_taskaffinity";
static final String AUTHORIZATION_IN_CURRENT_TASK = "authorization_in_current_task";
static final String WEBAUTHN_CAPABLE = "webauthn_capable";
static final String WEBAUTHN_VERSION = "webauthn_version";

}

@SerializedName(CLIENT_ID)
Expand Down Expand Up @@ -187,6 +190,10 @@ public static final class SerializedNames {
@SerializedName(WEBAUTHN_CAPABLE)
private Boolean webauthnCapable;


@SerializedName(WEBAUTHN_VERSION)
private String webauthnVersion;

transient private OAuth2TokenCache mOAuth2TokenCache;

transient private Context mAppContext;
Expand Down Expand Up @@ -430,6 +437,10 @@ public Boolean isWebauthnCapable() {
return Boolean.TRUE.equals(webauthnCapable);
}

public String getWebauthnVersion() {
return webauthnVersion;
}

public Authority getDefaultAuthority() {
if (mAuthorities != null) {
if (mAuthorities.size() > 1) {
Expand Down Expand Up @@ -515,6 +526,8 @@ public void mergeConfiguration(PublicClientApplicationConfiguration config) {
this.handleNullTaskAffinity = config.handleNullTaskAffinity == null ? this.handleNullTaskAffinity : config.handleNullTaskAffinity;
this.isAuthorizationInCurrentTask = config.isAuthorizationInCurrentTask == null ? this.isAuthorizationInCurrentTask : config.isAuthorizationInCurrentTask;
this.webauthnCapable = config.webauthnCapable == null ? this.webauthnCapable : config.webauthnCapable;
this.webauthnVersion = config.webauthnVersion == null ? this.webauthnVersion : config.webauthnVersion;

}

public void validateConfiguration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import com.microsoft.identity.client.AcquireTokenParameters;
import com.microsoft.identity.client.AcquireTokenSilentParameters;
Expand All @@ -33,6 +34,7 @@
import com.microsoft.identity.client.ITenantProfile;
import com.microsoft.identity.client.MultiTenantAccount;
import com.microsoft.identity.common.internal.platform.AndroidPlatformUtil;
import com.microsoft.identity.common.java.constants.FidoConstants;
import com.microsoft.identity.common.java.logging.DiagnosticContext;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITChallengeAuthMethodCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITContinueCommandParameters;
Expand Down Expand Up @@ -86,6 +88,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -204,6 +207,7 @@ public static InteractiveTokenCommandParameters createInteractiveTokenCommandPar
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
.correlationId(parameters.getCorrelationId())
.preferredAuthMethod(parameters.getPreferredAuthMethod())
.requestHeaders(addPasskeyHeader(parameters.getExtraQueryStringParameters(), configuration))
.build();
}

Expand Down Expand Up @@ -1371,4 +1375,81 @@ public static List<Map.Entry<String, String>> appendToExtraQueryParametersIfWebA
ArrayList<Map.Entry<String, String>> result = queryStringParameters != null ? new ArrayList<>(queryStringParameters) : new ArrayList<>();
return AndroidPlatformUtil.updateWithOrDeleteWebAuthnParam(result, configuration.isWebauthnCapable());
}


/**
* Adds passkey protocol headers if WebAuthn is enabled and supported (Android 9+, version 1.1).
*
* @param queryStringParameters Query parameters from the authentication request.
* @param configuration Application configuration with WebAuthn settings.
* @return HashMap with passkey headers if conditions are met, otherwise empty.
*/
@NonNull
private static HashMap<String, String> addPasskeyHeader(
@Nullable final List<Map.Entry<String, String>> queryStringParameters,
@NonNull final PublicClientApplicationConfiguration configuration) {

final String methodTag = TAG + ":addPasskeyHeader";
final HashMap<String, String> headers = new HashMap<>();

// Passkey functionality requires Android 9 (Pie) or higher
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return headers;
}

// Skip if not using WebView authorization agent
if (!AuthorizationAgent.WEBVIEW.equals(configuration.getAuthorizationAgent())) {
return headers;
}


// Skip if no webauthn query parameter and the configuration isn't webauthn-capable
if (!containsValidWebAuth(queryStringParameters) && !configuration.isWebauthnCapable()) {
return headers;
}

if (configuration.getWebauthnVersion() == null) {
return headers;
}

switch (configuration.getWebauthnVersion()) {
case FidoConstants.PASSKEY_PROTOCOL_VERSION_1_0:
headers.put(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME, FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_ONLY);
Logger.verbose(methodTag, "Passkey header added for WebAuthn version 1.0");
break;
case FidoConstants.PASSKEY_PROTOCOL_VERSION_1_1:
headers.put(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME, FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_AND_REG);
Logger.verbose(methodTag, "Passkey header added for WebAuthn version 1.1");
break;
default:
Logger.verbose(methodTag, "Unsupported WebAuthn version: " + configuration.getWebauthnVersion());
break;
}

return headers;
}

/**
* Determines whether the given list of query parameters contains a valid WebAuthn entry.
*
* @param queryParameters the list of query parameters to inspect, may be null.
* @return {@code true} if a parameter with both the expected WebAuthn key and value is found; {@code false} otherwise.
*/
private static boolean containsValidWebAuth(
@Nullable final List<Map.Entry<String, String>> queryParameters) {

if (queryParameters == null || queryParameters.isEmpty()) {
return false;
}

for (Map.Entry<String, String> entry : queryParameters) {
if (FidoConstants.WEBAUTHN_QUERY_PARAMETER_FIELD.equals(entry.getKey())
&& FidoConstants.WEBAUTHN_QUERY_PARAMETER_VALUE.equals(entry.getValue())) {
return true;
}
}

return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInWithContinuationTokenCommandParameters;
import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache;
import com.microsoft.identity.common.java.exception.ClientException;
import com.microsoft.identity.common.java.ui.AuthorizationAgent;
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
import com.microsoft.identity.msal.R;
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration;
Expand Down Expand Up @@ -365,6 +366,103 @@ public void testAppendToExtraQueryParametersIfWebAuthnCapable_WebAuthnCapableFal
Assert.assertEquals(combinedQueryParameters.size(), 1);
}


@Test
@Config(sdk=28)
public void testPasskeyHeader_AddedWhenWebAuthnConfigurationEnabled() throws ClientException {
InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
.createInteractiveTokenCommandParameters(
getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE),
getCache(),
getAcquireTokenParametersWithClaims()
);
Assert.assertTrue(commandParameters
.getRequestHeaders()
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
Assert.assertEquals(
FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_AND_REG,
commandParameters.getRequestHeaders().get(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
}

@Test
@Config(sdk=28)
public void testPasskeyHeader_NotAddedWhenAuthorizationAgentIsDefault() throws ClientException {
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
Mockito.when(mockConfig.getAuthorizationAgent()).thenReturn(AuthorizationAgent.DEFAULT);

InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
.createInteractiveTokenCommandParameters(
mockConfig,
getCache(),
getAcquireTokenParametersWithClaims()
);
Assert.assertFalse(commandParameters
.getRequestHeaders()
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
}

@Test
@Config(sdk=28)
public void testPasskeyHeader_NotAddedWhenWebAuthnNotCapable() throws ClientException {
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
Mockito.when(mockConfig.isWebauthnCapable()).thenReturn(false);

InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
.createInteractiveTokenCommandParameters(
mockConfig,
getCache(),
getAcquireTokenParametersWithClaims()
);
Assert.assertFalse(commandParameters
.getRequestHeaders()
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
}

@Test
@Config(sdk=28)
public void testPasskeyHeader_NotAddedWhenWebAuthnVersionUnsupported() throws ClientException {
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
Mockito.when(mockConfig.getWebauthnVersion()).thenReturn("3.0");

InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
.createInteractiveTokenCommandParameters(
mockConfig,
getCache(),
getAcquireTokenParametersWithClaims()
);
Assert.assertFalse(commandParameters
.getRequestHeaders()
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
}

@Test
@Config(sdk=28)
public void testPasskeyHeader_NotAddedWhenWebAuthnVersion1_0() throws ClientException {
PublicClientApplicationConfiguration mockConfig = Mockito.spy(getConfiguration(WEBAUTHN_CAPABLE_CONFIG_FILE));
Mockito.when(mockConfig.getWebauthnVersion()).thenReturn("1.0");

InteractiveTokenCommandParameters commandParameters = CommandParametersAdapter
.createInteractiveTokenCommandParameters(
mockConfig,
getCache(),
getAcquireTokenParametersWithClaims()
);
Assert.assertTrue(commandParameters
.getRequestHeaders()
.containsKey(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
Assert.assertEquals(
FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_ONLY,
commandParameters.getRequestHeaders().get(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME)
);
}


@Test
public void testCreateSignInStartCommandParameters_CommandParamsContainsExpectedParams() throws ClientException {
List<String> challengeTypes = new ArrayList<>(Collections.singletonList("OOB"));
Expand Down
3 changes: 2 additions & 1 deletion msal/src/test/res/raw/webauthn_capable.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"client_id" : "4b0db8c2-9f26-4417-8bde-3f0e3656f8e0",
"authorization_user_agent" : "DEFAULT",
"authorization_user_agent" : "WEBVIEW",
"redirect_uri" : "msauth://com.microsoft.identity.client.sample.local/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
"multiple_clouds_supported":true,
"broker_redirect_uri_registered": true,
"account_mode": "MULTIPLE",
"webauthn_capable": true,
"webauthn_version": 1.1,
"authorities" : [
{
"type": "AAD",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ enum ConfigFile {
WEBVIEW_WITH_PPE,

WEBVIEW_PPE_MSA,
ONEBOX
ONEBOX,

WEBVIEW_MSA_PASSKEY_REG
}

public static int getResourceIdFromConfigFile(ConfigFile configFile) {
Expand Down Expand Up @@ -124,6 +126,10 @@ public static int getResourceIdFromConfigFile(ConfigFile configFile) {

case ONEBOX:
return R.raw.msal_config_onebox;

case WEBVIEW_MSA_PASSKEY_REG:
return R.raw.msal_config_webview_msa_passkey_reg;

}

return R.raw.msal_config_default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
import com.microsoft.identity.client.exception.MsalServiceException;
import com.microsoft.identity.client.exception.MsalUiRequiredException;
import com.microsoft.identity.common.internal.ui.browser.AndroidBrowserSelector;
import com.microsoft.identity.common.java.authorities.Environment;
import com.microsoft.identity.common.java.browser.Browser;
import com.microsoft.identity.common.java.constants.FidoConstants;
import com.microsoft.identity.common.java.exception.BaseException;
import com.microsoft.identity.common.java.ui.AuthorizationAgent;
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
import com.microsoft.identity.common.java.util.StringUtil;

Expand Down Expand Up @@ -167,6 +170,18 @@ private AcquireTokenParameters.Builder getAcquireTokenParametersBuilder(@NonNull
if (requestOptions.isAllowSignInFromOtherDevice()) {
extraQP.add(new AbstractMap.SimpleEntry<>("is_remote_login_allowed", Boolean.toString(true)));
}

// Add "msaoauth2=true" to test WebAuthN on WebView PPE
final String webauthnVersion = getApp().getConfiguration().getWebauthnVersion();
final Environment environment = getApp().getConfiguration().getEnvironment();
final AuthorizationAgent authorizationAgent = getApp().getConfiguration().getAuthorizationAgent();
if (getApp().getConfiguration().isWebauthnCapable()
&& FidoConstants.PASSKEY_PROTOCOL_VERSION_1_1.equals(webauthnVersion)
&& Environment.PreProduction.equals(environment)
&& AuthorizationAgent.WEBVIEW.equals(authorizationAgent)) {
extraQP.add(new AbstractMap.SimpleEntry<>("msaoauth2", Boolean.toString(true)));
}

builder.withAuthorizationQueryStringParameters(extraQP);

if (!StringUtil.isNullOrEmpty(requestOptions.getAuthority())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"client_id" : "9668f2bd-6103-4292-9024-84fa2d1b6fb2",
"authorization_user_agent" : "WEBVIEW",
"redirect_uri" : "msauth://com.msft.identity.client.sample.local/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
"webauthn_capable" : true,
"webauthn_version" : 1.1,
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"authorization_user_agent" : "WEBVIEW",
"redirect_uri" : "msauth://com.msft.identity.client.sample.local/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
"webauthn_capable" : true,
"webauthn_version" : "1.1",
"environment" : "PreProduction",
"authorities" : [
{
Expand Down