Skip to content

Commit

Permalink
Native auth: Add support for claims request, Fixes AB#3117218 (#2246)
Browse files Browse the repository at this point in the history
Adding support on native authentication for claims request. During
sign-in, native authentication users can now specify claims requests,
which will be sent to the /token endpoint. This enhancement specifically
supports authentication context.

MSAL Common PR:
AzureAD/microsoft-authentication-library-common-for-android#2572


[AB#3117218](https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3117218)
  • Loading branch information
nilo-ms authored Jan 23, 2025
1 parent bcda2d7 commit c1eafb7
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 17 deletions.
1 change: 1 addition & 0 deletions changelog
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr
vNext
----------
- [MINOR] Move native auth public methods to parameter class (#2245)
- [MINOR] Add support for claims requests for native auth sign in (#2246)

Version 5.9.0
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,17 @@ public static SignInStartCommandParameters createSignInStartCommandParameters(
@NonNull final OAuth2TokenCache tokenCache,
@NonNull final String username,
@Nullable final char[] password,
final List<String> scopes) throws ClientException {
final List<String> scopes,
@Nullable final ClaimsRequest claimsRequest) throws ClientException {
final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme(
AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()),
null
);

final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority());

final String claimsRequestJson = ClaimsRequest.getJsonStringFromClaimsRequest(claimsRequest);

final SignInStartCommandParameters commandParameters = SignInStartCommandParameters.builder()
.platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()))
.applicationName(configuration.getAppContext().getPackageName())
Expand All @@ -565,6 +568,7 @@ public static SignInStartCommandParameters createSignInStartCommandParameters(
.authenticationScheme(authenticationScheme)
.clientId(configuration.getClientId())
.challengeType(configuration.getChallengeTypes())
.claimsRequestJson(claimsRequestJson)
.scopes(scopes)
// Start of the flow, so there is no correlation ID to use from a previous API response.
// Set it to a default value.
Expand Down Expand Up @@ -640,7 +644,8 @@ public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandPar
@NonNull final String code,
@NonNull final String continuationToken,
@NonNull final String correlationId,
final List<String> scopes) throws ClientException {
final List<String> scopes,
@Nullable final String claimsRequestJson) throws ClientException {

final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority());

Expand Down Expand Up @@ -668,6 +673,7 @@ public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandPar
.code(code)
.scopes(scopes)
.correlationId(correlationId)
.claimsRequestJson(claimsRequestJson)
.build();

return commandParameters;
Expand Down Expand Up @@ -729,7 +735,8 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo
@NonNull final String continuationToken,
@NonNull final char[] password,
@NonNull final String correlationId,
final List<String> scopes) throws ClientException {
final List<String> scopes,
@Nullable final String claimsRequestJson) throws ClientException {

final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority());

Expand Down Expand Up @@ -758,6 +765,7 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo
.scopes(scopes)
.challengeType(configuration.getChallengeTypes())
.correlationId(correlationId)
.claimsRequestJson(claimsRequestJson)
.build();

return commandParameters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.microsoft.identity.client.AccountAdapter
import com.microsoft.identity.client.AuthenticationResultAdapter
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.claims.ClaimsRequest
import com.microsoft.identity.client.exception.MsalClientException
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.internal.CommandParametersAdapter
Expand Down Expand Up @@ -309,7 +310,7 @@ class NativeAuthPublicClientApplication(
)
pcaScope.launch {
try {
val result = internalSignIn(username, password, scopes)
val result = internalSignIn(username, password, scopes, claimsRequest = null)
callback.onResult(result)
} catch (e: MsalException) {
Logger.error(TAG, "Exception thrown in signIn", e)
Expand All @@ -333,7 +334,7 @@ class NativeAuthPublicClientApplication(
)
pcaScope.launch {
try {
val result = internalSignIn(parameters.username, parameters.password, parameters.scopes)
val result = internalSignIn(parameters.username, parameters.password, parameters.scopes, parameters.claimsRequest)
callback.onResult(result)
} catch (e: MsalException) {
Logger.error(TAG, "Exception thrown in signIn", e)
Expand Down Expand Up @@ -362,7 +363,7 @@ class NativeAuthPublicClientApplication(
correlationId = null,
methodName = "${TAG}.signIn(username: String, password: CharArray?, scopes: List<String>?)"
)
return internalSignIn(username, password, scopes)
return internalSignIn(username, password, scopes, claimsRequest = null)
}

/**
Expand All @@ -378,7 +379,7 @@ class NativeAuthPublicClientApplication(
correlationId = null,
methodName = "${TAG}.signIn(parameters: NativeAuthSignInParameters)"
)
return internalSignIn(parameters.username, parameters.password, parameters.scopes)
return internalSignIn(parameters.username, parameters.password, parameters.scopes, parameters.claimsRequest)
}


Expand Down Expand Up @@ -605,7 +606,8 @@ class NativeAuthPublicClientApplication(
private suspend fun internalSignIn(
username: String,
password: CharArray?,
scopes: List<String>?
scopes: List<String>?,
claimsRequest: ClaimsRequest?
): SignInResult {
return withContext(Dispatchers.IO) {
try {
Expand All @@ -627,13 +629,14 @@ class NativeAuthPublicClientApplication(
nativeAuthConfig.oAuth2TokenCache,
username,
password,
scopes
scopes,
claimsRequest
)

val command = SignInStartCommand(
params,
NativeAuthMsalController(),
PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL
if (hasPassword) PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL_PASSWORD else PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL
)

val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get()
Expand Down Expand Up @@ -679,7 +682,8 @@ class NativeAuthPublicClientApplication(
continuationToken = result.continuationToken,
correlationId = result.correlationId,
scopes = scopes,
config = nativeAuthConfig
config = nativeAuthConfig,
claimsRequestJson = params.claimsRequestJson
),
codeLength = result.codeLength,
sentTo = result.challengeTargetLabel,
Expand Down Expand Up @@ -715,7 +719,8 @@ class NativeAuthPublicClientApplication(
continuationToken = result.continuationToken,
correlationId = result.correlationId,
scopes = scopes,
config = nativeAuthConfig
config = nativeAuthConfig,
claimsRequestJson = params.claimsRequestJson
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@

package com.microsoft.identity.nativeauth.parameters

import com.microsoft.identity.client.claims.ClaimsRequest

/**
* Encapsulates the parameters passed to the signIn methods of NativeAuthPublicClientApplication
*/
class NativeAuthSignInParameters(
/**
* username of the account to sign in.
* username of the account to sign in
*/
val username: String
) {
Expand All @@ -43,4 +45,9 @@ class NativeAuthSignInParameters(
* Not all scopes are guaranteed to be included in the access token returned.
*/
var scopes: List<String>? = null

/**
* The claims parameter that needs to be sent to the service.
*/
var claimsRequest: ClaimsRequest? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class SignInCodeRequiredState internal constructor(
override val continuationToken: String,
override val correlationId: String,
private val scopes: List<String>?,
private val claimsRequestJson: String?,
private val config: NativeAuthPublicClientApplicationConfiguration
) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable {
private val TAG: String = SignInCodeRequiredState::class.java.simpleName
Expand All @@ -84,6 +85,7 @@ class SignInCodeRequiredState internal constructor(
continuationToken = parcel.readString() ?: "",
correlationId = parcel.readString() ?: "UNSET",
scopes = parcel.createStringArrayList(),
claimsRequestJson = parcel.readString(),
config = parcel.serializable<NativeAuthPublicClientApplicationConfiguration>() as NativeAuthPublicClientApplicationConfiguration
)

Expand Down Expand Up @@ -136,7 +138,8 @@ class SignInCodeRequiredState internal constructor(
code,
continuationToken,
correlationId,
scopes
scopes,
claimsRequestJson
)

val signInSubmitCodeCommand = SignInSubmitCodeCommand(
Expand Down Expand Up @@ -274,7 +277,8 @@ class SignInCodeRequiredState internal constructor(
continuationToken = result.continuationToken,
correlationId = result.correlationId,
scopes = scopes,
config = config
config = config,
claimsRequestJson = claimsRequestJson
),
codeLength = result.codeLength,
sentTo = result.challengeTargetLabel,
Expand Down Expand Up @@ -344,13 +348,15 @@ class SignInPasswordRequiredState(
override val continuationToken: String,
override val correlationId: String,
private val scopes: List<String>?,
private val claimsRequestJson: String?,
private val config: NativeAuthPublicClientApplicationConfiguration
) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable {
private val TAG: String = SignInPasswordRequiredState::class.java.simpleName
constructor(parcel: Parcel) : this(
continuationToken = parcel.readString() ?: "",
correlationId = parcel.readString() ?: "UNSET",
scopes = parcel.createStringArrayList(),
claimsRequestJson = parcel.readString(),
config = parcel.serializable<NativeAuthPublicClientApplicationConfiguration>() as NativeAuthPublicClientApplicationConfiguration
)

Expand Down Expand Up @@ -403,7 +409,8 @@ class SignInPasswordRequiredState(
continuationToken,
password,
correlationId,
scopes
scopes,
claimsRequestJson
)

try {
Expand Down Expand Up @@ -483,6 +490,7 @@ class SignInPasswordRequiredState(
parcel.writeString(correlationId)
parcel.writeStringList(scopes)
parcel.writeSerializable(config)
parcel.writeString(claimsRequestJson)
}

override fun describeContents(): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@
import com.microsoft.identity.common.java.commands.parameters.InteractiveTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.SilentTokenCommandParameters;
import com.microsoft.identity.common.java.constants.FidoConstants;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInStartCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInSubmitCodeCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInSubmitPasswordCommandParameters;
import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache;
import com.microsoft.identity.common.java.exception.ClientException;
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
import com.microsoft.identity.msal.R;
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration;
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfigurationFactory;

import org.junit.Assert;
import org.junit.Before;
Expand Down Expand Up @@ -358,6 +364,96 @@ public void testAppendToExtraQueryParametersIfWebAuthnCapable_WebAuthnCapableFal
Assert.assertEquals(combinedQueryParameters.size(), 1);
}

@Test
public void testCreateSignInStartCommandParameters_CommandParamsContainsExpectedParams() throws ClientException {
List<String> challengeTypes = new ArrayList<>(Collections.singletonList("OOB"));
String username = "username";
char[] pwd = "example".toCharArray();
List<String> scopes = new ArrayList<>(Collections.singletonList("User.Read"));
ClaimsRequest claimsRequest = ClaimsRequest.getClaimsRequestFromJsonString("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"c4\"}}}");
NativeAuthPublicClientApplicationConfiguration configuration = new NativeAuthPublicClientApplicationConfiguration();
configuration.setChallengeTypes(challengeTypes);
configuration.setClientId("clientId");
configuration.setAppContext(mContext);
configuration.setPowerOptCheckEnabled(false);

final SignInStartCommandParameters commandParameters = CommandParametersAdapter.createSignInStartCommandParameters(
configuration,
null,
username,
pwd,
scopes,
claimsRequest
);
Assert.assertEquals(commandParameters.claimsRequestJson, ClaimsRequest.getJsonStringFromClaimsRequest(claimsRequest));
Assert.assertEquals(commandParameters.password, pwd);
Assert.assertEquals(commandParameters.username, username);
Assert.assertEquals(commandParameters.scopes, scopes);
Assert.assertEquals(commandParameters.challengeType, challengeTypes);
}

@Test
public void createSignInSubmitCodeCommandParameters_CommandParamsContainsExpectedParams() throws ClientException {
List<String> challengeTypes = new ArrayList<>(Collections.singletonList("OOB"));
String code = "123456";
String continuationToken = "continuationToken";
String correlationId = UUID.randomUUID().toString();
List<String> scopes = new ArrayList<>(Collections.singletonList("User.Read"));
String claimsRequestJson = "{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"c4\"}}}";
NativeAuthPublicClientApplicationConfiguration configuration = new NativeAuthPublicClientApplicationConfiguration();
configuration.setChallengeTypes(challengeTypes);
configuration.setClientId("clientId");
configuration.setAppContext(mContext);
configuration.setPowerOptCheckEnabled(false);

final SignInSubmitCodeCommandParameters commandParameters = CommandParametersAdapter.createSignInSubmitCodeCommandParameters(
configuration,
null,
code,
continuationToken,
correlationId,
scopes,
claimsRequestJson
);
Assert.assertEquals(commandParameters.claimsRequestJson, claimsRequestJson);
Assert.assertEquals(commandParameters.code, code);
Assert.assertEquals(commandParameters.continuationToken, continuationToken);
Assert.assertEquals(commandParameters.scopes, scopes);
Assert.assertEquals(commandParameters.challengeType, challengeTypes);
Assert.assertEquals(commandParameters.getCorrelationId(), correlationId);
}

@Test
public void createSignInSubmitPasswordCommandParameters_CommandParamsContainsExpectedParams() throws ClientException {
List<String> challengeTypes = new ArrayList<>(Collections.singletonList("OOB"));
char[] pwd = "example".toCharArray();
String continuationToken = "continuationToken";
String correlationId = UUID.randomUUID().toString();
List<String> scopes = new ArrayList<>(Collections.singletonList("User.Read"));
String claimsRequestJson = "{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"c4\"}}}";
NativeAuthPublicClientApplicationConfiguration configuration = new NativeAuthPublicClientApplicationConfiguration();
configuration.setChallengeTypes(challengeTypes);
configuration.setClientId("clientId");
configuration.setAppContext(mContext);
configuration.setPowerOptCheckEnabled(false);

final SignInSubmitPasswordCommandParameters commandParameters = CommandParametersAdapter.createSignInSubmitPasswordCommandParameters(
configuration,
null,
continuationToken,
pwd,
correlationId,
scopes,
claimsRequestJson
);
Assert.assertEquals(commandParameters.claimsRequestJson, claimsRequestJson);
Assert.assertEquals(commandParameters.password, pwd);
Assert.assertEquals(commandParameters.continuationToken, continuationToken);
Assert.assertEquals(commandParameters.scopes, scopes);
Assert.assertEquals(commandParameters.challengeType, challengeTypes);
Assert.assertEquals(commandParameters.getCorrelationId(), correlationId);
}

private ClaimsRequest getAccessTokenClaimsRequest(@NonNull String claimName, @NonNull String claimValue) {
ClaimsRequest cp1ClaimsRequest = new ClaimsRequest();
RequestedClaimAdditionalInformation info = new RequestedClaimAdditionalInformation();
Expand Down

0 comments on commit c1eafb7

Please sign in to comment.