From d35cc172797cf7d5cc67696d6765b6f273983511 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Thu, 11 Sep 2025 16:03:32 +0100 Subject: [PATCH 01/17] update public interface with new MFA flow. Auth method is always needed. No getAuthMethod methods --- common | 2 +- .../internal/CommandParametersAdapter.java | 57 +------ .../NativeAuthPublicClientApplication.kt | 3 +- .../statemachine/errors/MFAErrors.kt | 22 --- .../statemachine/results/MFAResult.kt | 8 +- .../statemachine/results/SignInResult.kt | 7 +- .../statemachine/states/MFAStates.kt | 161 ++---------------- .../statemachine/states/SignInStates.kt | 3 +- 8 files changed, 31 insertions(+), 232 deletions(-) diff --git a/common b/common index cfc0979888..45b2f179d0 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit cfc0979888b093da9572f1f4e07bcae67800b7ce +Subproject commit 45b2f179d0dc226c217c1f09050096e395cb9425 diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index 28d41e459f..ea79960c80 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -62,8 +62,7 @@ import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority; import com.microsoft.identity.common.java.nativeauth.commands.parameters.GetAuthMethodsCommandParameters; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFADefaultChallengeCommandParameters; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFASelectedDefaultChallengeCommandParameters; +import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFAChallengeAuthMethodCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFASubmitChallengeCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordResendCodeCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordStartCommandParameters; @@ -778,54 +777,6 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo return commandParameters; } - /** - * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand}] of Native Auth - * @param configuration PCA configuration - * @param tokenCache token cache for storing results - * @param correlationId correlation ID to use in the API request, taken from the previous request in the flow - * @param continuationToken continuation token - * @param scopes scopes requested during sign in flow - * @return Command parameter object - * @throws ClientException - */ - public static MFADefaultChallengeCommandParameters createMFADefaultChallengeCommandParameters( - @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, - @NonNull final OAuth2TokenCache tokenCache, - @NonNull final String continuationToken, - @NonNull final String correlationId, - final List scopes) throws ClientException { - - final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - - final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( - AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), - null - ); - - final MFADefaultChallengeCommandParameters commandParameters = - MFADefaultChallengeCommandParameters.builder() - .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) - .applicationName(configuration.getAppContext().getPackageName()) - .applicationVersion(getPackageVersion(configuration.getAppContext())) - .clientId(configuration.getClientId()) - .isSharedDevice(configuration.getIsSharedDevice()) - .redirectUri(configuration.getRedirectUri()) - .oAuth2TokenCache(tokenCache) - .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) - .sdkType(SdkType.MSAL) - .sdkVersion(PublicClientApplication.getSdkVersion()) - .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) - .authority(authority) - .authenticationScheme(authenticationScheme) - .continuationToken(continuationToken) - .scopes(scopes) - .challengeType(configuration.getChallengeTypes()) - .correlationId(correlationId) - .build(); - - return commandParameters; - } - /** * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand}] of Native Auth * @param configuration PCA configuration @@ -836,7 +787,7 @@ public static MFADefaultChallengeCommandParameters createMFADefaultChallengeComm * @return Command parameter object * @throws ClientException */ - public static MFASelectedDefaultChallengeCommandParameters createMFASelectedChallengeCommandParameters( + public static MFAChallengeAuthMethodCommandParameters createMFASelectedChallengeCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @NonNull final String continuationToken, @@ -853,8 +804,8 @@ public static MFASelectedDefaultChallengeCommandParameters createMFASelectedChal final String authMethodId = authMethod.getId(); - final MFASelectedDefaultChallengeCommandParameters commandParameters = - MFASelectedDefaultChallengeCommandParameters.builder() + final MFAChallengeAuthMethodCommandParameters commandParameters = + MFAChallengeAuthMethodCommandParameters.builder() .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) .applicationName(configuration.getAppContext().getPackageName()) .applicationVersion(getPackageVersion(configuration.getAppContext())) diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt index 45cfe98667..497a72039b 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt @@ -768,7 +768,8 @@ class NativeAuthPublicClientApplication( correlationId = result.correlationId, scopes = scopes, config = nativeAuthConfig - ) + ), + authMethods = result.authMethods.toListOfAuthMethods() ) } diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt index b6c6a583f6..27ffe9b2bf 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt @@ -1,6 +1,5 @@ package com.microsoft.identity.nativeauth.statemachine.errors -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult @@ -26,27 +25,6 @@ class MFARequestChallengeError( override var exception: Exception? = null ): MFARequiredResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) -/** - * MFA get authentication methods error. Use the utility methods of this class - * to identify and handle the error. This error is produced by - * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods] - * @param errorType the error type value of the error that occurred. - * @param error the error returned by the authentication server. - * @param errorMessage the error message returned by the authentication server. - * @param correlationId a unique identifier for the request that can help in diagnostics. - * @param errorCodes a list of specific error codes returned by the authentication server. - * @param exception an internal unexpected exception that happened. - */ -class MFAGetAuthMethodsError( - override val errorType: String? = null, - override val error: String? = null, - override val errorMessage: String?, - override val correlationId: String, - override val errorCodes: List? = null, - val subError: String? = null, - override var exception: Exception? = null -): MFAGetAuthMethodsResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) - /** * MFA submit challenge error. The user should use the utility methods of this class * to identify and handle the error. This error is produced by diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt index 6c87895b62..32b233d2f7 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt @@ -57,15 +57,9 @@ interface MFARequiredResult: Result { class SelectionRequired( override val nextState: MFARequiredState, val authMethods: List - ) : MFARequiredResult, MFAGetAuthMethodsResult, Result.SuccessResult(nextState = nextState) + ) : MFARequiredResult, Result.SuccessResult(nextState = nextState) } -/** - * Results related to get authentication methods operation, produced by - * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods] - */ -interface MFAGetAuthMethodsResult : Result - /** * Results related to MFA submit challenge operation, produced by * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.submitChallenge] diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt index 9c32fd6751..2e1e79f9a3 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt @@ -82,8 +82,9 @@ interface SignInResult : Result { * @param nextState [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState] the current state of the flow with follow-on methods. */ class MFARequired( - override val nextState: AwaitingMFAState - ) : SignInResult, Result.SuccessResult(nextState = nextState), SignInSubmitPasswordResult + override val nextState: AwaitingMFAState, + val authMethods: List + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult /** * StrongAuthMethodRegistration Result, which indicates that a registration of a strong authentication method is required to continue. @@ -94,7 +95,7 @@ interface SignInResult : Result { class StrongAuthMethodRegistrationRequired( override val nextState: RegisterStrongAuthState, val authMethods: List - ) : SignInResult, SignInSubmitPasswordResult, Result.SuccessResult(nextState = nextState) + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult } /** diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt index 2dbcaf149a..2c9a0d9be9 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt @@ -32,14 +32,12 @@ import com.microsoft.identity.common.java.controllers.CommandDispatcher import com.microsoft.identity.common.java.eststelemetry.PublicApiId import com.microsoft.identity.common.java.logging.LogSession import com.microsoft.identity.common.java.logging.Logger -import com.microsoft.identity.common.java.nativeauth.controllers.results.GetAuthMethodsCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.INativeAuthCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFAChallengeCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFACommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFASubmitChallengeCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.SignInCommandResult import com.microsoft.identity.common.java.nativeauth.util.checkAndWrapCommandResultType -import com.microsoft.identity.common.nativeauth.internal.commands.GetAuthMethodsCommand import com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand import com.microsoft.identity.common.nativeauth.internal.commands.MFASubmitChallengeCommand import com.microsoft.identity.common.nativeauth.internal.controllers.NativeAuthMsalController @@ -47,10 +45,8 @@ import com.microsoft.identity.nativeauth.AuthMethod import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes -import com.microsoft.identity.nativeauth.statemachine.errors.MFAGetAuthMethodsError import com.microsoft.identity.nativeauth.statemachine.errors.MFARequestChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult @@ -77,10 +73,11 @@ class AwaitingMFAState( * Requests a challenge to be sent to the user's default authentication method; callback variant. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. + * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. * @param callback [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState.RequestChallengeCallback] to receive the result on. * @return The result of the request challenge action. */ - fun requestChallenge(callback: RequestChallengeCallback) { + fun requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback) { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, @@ -88,7 +85,7 @@ class AwaitingMFAState( ) NativeAuthPublicClientApplication.pcaScope.launch { try { - val result = requestChallenge() + val result = requestChallenge(authMethod) callback.onResult(result) } catch (e: MsalException) { Logger.error(TAG, "Exception thrown in requestChallenge", e) @@ -103,7 +100,7 @@ class AwaitingMFAState( * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @return The result of the request challenge action. */ - suspend fun requestChallenge(): MFARequiredResult { + suspend fun requestChallenge(authMethod: AuthMethod): MFARequiredResult { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, @@ -114,17 +111,17 @@ class AwaitingMFAState( return withContext(Dispatchers.IO) { try { - val params = CommandParametersAdapter.createMFADefaultChallengeCommandParameters( + val params = CommandParametersAdapter.createMFASelectedChallengeCommandParameters( config, config.oAuth2TokenCache, continuationToken, correlationId, - scopes + authMethod ) val command = MFAChallengeCommand( parameters = params, controller = NativeAuthMsalController(), - publicApiId = PublicApiId.NATIVE_AUTH_MFA_DEFAULT_CHALLENGE + publicApiId = PublicApiId.NATIVE_AUTH_MFA_SELECTED_CHALLENGE ) val rawCommandResult = @@ -229,116 +226,6 @@ class MFARequiredState( ) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable { private val TAG: String = MFARequiredState::class.java.simpleName - /** - * GetAuthMethodsCallback receives the result for getAuthMethods() in MFA flows in native authentication. - */ - interface GetAuthMethodsCallback : Callback - - /** - * Retrieves all authentication methods that can be used to complete the challenge flow; callback variant. - * - * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. - * @param callback [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.GetAuthMethodsCallback] to receive the result on. - * @return The results of the get authentication methods action. - */ - fun getAuthMethods(callback: GetAuthMethodsCallback) { - LogSession.logMethodCall( - tag = TAG, - correlationId = correlationId, - methodName = "${TAG}.getAuthMethods(callback: GetAuthMethodsCallback)" - ) - NativeAuthPublicClientApplication.pcaScope.launch { - try { - val result = getAuthMethods() - callback.onResult(result) - } catch (e: MsalException) { - Logger.error(TAG, "Exception thrown in getAuthMethods", e) - callback.onError(e) - } - } - } - - /** - * Retrieves all authentication methods that can be used to complete the challenge flow; Kotlin coroutines variant. - * - * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. - * @return The results of the get authentication methods action. - */ - suspend fun getAuthMethods(): MFAGetAuthMethodsResult { - LogSession.logMethodCall( - tag = TAG, - correlationId = correlationId, - methodName = "${TAG}.getAuthMethods()" - ) - - Logger.warn(TAG, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications.") - - return withContext(Dispatchers.IO) { - try { - val params = CommandParametersAdapter.createGetAuthMethodsCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId - ) - val command = GetAuthMethodsCommand( - parameters = params, - controller = NativeAuthMsalController(), - publicApiId = PublicApiId.NATIVE_AUTH_GET_AUTH_METHODS - ) - - val rawCommandResult = - CommandDispatcher.submitSilentReturningFuture(command) - .get() - - return@withContext when (val result = - rawCommandResult.checkAndWrapCommandResultType()) { - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } - is INativeAuthCommandResult.APIError -> { - Logger.warnWithObject( - TAG, - result.correlationId, - "getAuthMethods() received unexpected result: ", - result - ) - MFAGetAuthMethodsError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - errorCodes = result.errorCodes, - exception = result.exception - ) - } - is INativeAuthCommandResult.Redirect -> { - MFAGetAuthMethodsError( - errorType = ErrorTypes.BROWSER_REQUIRED, - error = result.error, - errorMessage = result.redirectReason, - correlationId = result.correlationId - ) - } - } - } catch (e: Exception) { - MFAGetAuthMethodsError( - errorType = ErrorTypes.CLIENT_EXCEPTION, - errorMessage = "MSAL client exception occurred in getAuthMethods().", - exception = e, - correlationId = correlationId - ) - } - } - } - /** * RequestChallengeCallback receives the result for requestChallenge() in MFA flows in native authentication. */ @@ -346,15 +233,13 @@ class MFARequiredState( /** * Requests a challenge to be sent to the user's default authentication method; callback variant. - * If an authentication method ID was supplied, the server will send a challenge to the specified method. If no ID is supplied, - * the server will attempt to send the challenge to the user's default auth method. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. - * @param callback [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.RequestChallengeCallback] to receive the result on. + * @param callback [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState.RequestChallengeCallback] to receive the result on. * @return The result of the request challenge action. */ - fun requestChallenge(authMethod: AuthMethod? = null, callback: RequestChallengeCallback) { + fun requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback) { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, @@ -373,14 +258,12 @@ class MFARequiredState( /** * Requests a challenge to be sent to the user's default authentication method; Kotlin coroutines variant. - * If an authentication method ID was supplied, the server will send a challenge to the specified method. If no ID is supplied, - * the server will attempt to send the challenge to the user's default auth method. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. * @return The result of the request challenge action. */ - suspend fun requestChallenge(authMethod: AuthMethod? = null): MFARequiredResult { + suspend fun requestChallenge(authMethod: AuthMethod): MFARequiredResult { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, @@ -391,23 +274,13 @@ class MFARequiredState( return withContext(Dispatchers.IO) { try { - val params = if (authMethod != null) { - CommandParametersAdapter.createMFASelectedChallengeCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId, - authMethod - ) - } else { - CommandParametersAdapter.createMFADefaultChallengeCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId, - scopes - ) - } + val params = CommandParametersAdapter.createMFASelectedChallengeCommandParameters( + config, + config.oAuth2TokenCache, + continuationToken, + correlationId, + authMethod + ) val command = MFAChallengeCommand( parameters = params, diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt index 8b74cc60b2..ca267ce630 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt @@ -452,7 +452,8 @@ class SignInPasswordRequiredState( correlationId = result.correlationId, scopes = scopes, config = config - ) + ), + authMethods = result.authMethods.toListOfAuthMethods() ) } is SignInCommandResult.StrongAuthMethodRegistrationRequired -> { From e2d80327bd323dbe6ee10a5a5117489dcb86938f Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Thu, 11 Sep 2025 16:15:32 +0100 Subject: [PATCH 02/17] adjust logs and removing selection required as possible results --- common | 2 +- .../statemachine/results/MFAResult.kt | 13 -------- .../statemachine/states/MFAStates.kt | 31 +++---------------- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/common b/common index 45b2f179d0..ab1ee6b7fd 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 45b2f179d0dc226c217c1f09050096e395cb9425 +Subproject commit ab1ee6b7fd43cc1486fcec4315157d121986100a diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt index 32b233d2f7..f43a36c385 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt @@ -23,7 +23,6 @@ package com.microsoft.identity.nativeauth.statemachine.results -import com.microsoft.identity.nativeauth.AuthMethod import com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState /** @@ -46,18 +45,6 @@ interface MFARequiredResult: Result { val sentTo: String, val channel: String, ) : MFARequiredResult, Result.SuccessResult(nextState = nextState) - - /** - * Selection required result, which indicates that a specific authentication method must be selected, which - * the server will send the challenge to (once sendChallenge() is called). - * - * @param nextState [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState] the current state of the flow with follow-on methods. - * @param authMethods the authentication methods that can be used to complete the challenge flow. - */ - class SelectionRequired( - override val nextState: MFARequiredState, - val authMethods: List - ) : MFARequiredResult, Result.SuccessResult(nextState = nextState) } /** diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt index 2c9a0d9be9..d2781f2e6d 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt @@ -50,7 +50,6 @@ import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeE import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult -import com.microsoft.identity.nativeauth.toListOfAuthMethods import com.microsoft.identity.nativeauth.utils.serializable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -81,7 +80,7 @@ class AwaitingMFAState( LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge(callback: RequestChallengeCallback)" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback)" ) NativeAuthPublicClientApplication.pcaScope.launch { try { @@ -104,7 +103,7 @@ class AwaitingMFAState( LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge()" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod)" ) Logger.warn(TAG, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications.") @@ -143,22 +142,11 @@ class AwaitingMFAState( channel = result.challengeChannel ) } - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } is INativeAuthCommandResult.APIError -> { Logger.warnWithObject( TAG, result.correlationId, - "requestChallenge() received unexpected result: ", + "requestChallenge(authMethod: AuthMethod) received unexpected result: ", result ) MFARequestChallengeError( @@ -243,7 +231,7 @@ class MFARequiredState( LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge(callback: RequestChallengeCallback)" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback)" ) NativeAuthPublicClientApplication.pcaScope.launch { try { @@ -307,17 +295,6 @@ class MFARequiredState( channel = result.challengeChannel ) } - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } is INativeAuthCommandResult.APIError -> { Logger.warnWithObject( TAG, From cbde5db242eada44014b2b299478ba0bf51b2ba7 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Thu, 11 Sep 2025 18:15:10 +0100 Subject: [PATCH 03/17] fix unit tests --- common | 2 +- .../internal/CommandParametersAdapter.java | 39 -- .../tests/network/nativeauth/SignInMFATest.kt | 147 +------- ...veAuthPublicClientApplicationJavaTest.java | 154 +------- ...veAuthPublicClientApplicationKotlinTest.kt | 346 ++---------------- 5 files changed, 34 insertions(+), 654 deletions(-) diff --git a/common b/common index ab1ee6b7fd..36fb764d37 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit ab1ee6b7fd43cc1486fcec4315157d121986100a +Subproject commit 36fb764d37a78039d3411d0589f8caca222986a8 diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index ea79960c80..7ce37325db 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -61,7 +61,6 @@ import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.GetAuthMethodsCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFAChallengeAuthMethodCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFASubmitChallengeCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordResendCodeCommandParameters; @@ -878,44 +877,6 @@ public static MFASubmitChallengeCommandParameters createMFASubmitChallengeComman return commandParameters; } - /** - * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.GetAuthMethodsCommand}] of Native Auth - * @param configuration PCA configuration - * @param tokenCache token cache for storing results - * @param correlationId correlation ID to use in the API request, taken from the previous request in the flow - * @param continuationToken Continuation token - * @return Command parameter object - */ - public static GetAuthMethodsCommandParameters createGetAuthMethodsCommandParameters( - @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, - @NonNull final OAuth2TokenCache tokenCache, - @NonNull final String continuationToken, - @NonNull final String correlationId) { - - final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - - final GetAuthMethodsCommandParameters commandParameters = - GetAuthMethodsCommandParameters.builder() - .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) - .applicationName(configuration.getAppContext().getPackageName()) - .applicationVersion(getPackageVersion(configuration.getAppContext())) - .clientId(configuration.getClientId()) - .isSharedDevice(configuration.getIsSharedDevice()) - .redirectUri(configuration.getRedirectUri()) - .oAuth2TokenCache(tokenCache) - .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) - .sdkType(SdkType.MSAL) - .sdkVersion(PublicClientApplication.getSdkVersion()) - .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) - .authority(authority) - .continuationToken(continuationToken) - .challengeType(configuration.getChallengeTypes()) - .correlationId(correlationId) - .build(); - - return commandParameters; - } - /** * Creates command parameter for [ResetPasswordStartCommand] of Native Auth. * @param configuration PCA configuration diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt index 8847577d2a..fdb0d595b4 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt @@ -32,15 +32,11 @@ import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthGetAccessTokenParameters import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInParameters import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError -import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.SignInError -import com.microsoft.identity.nativeauth.statemachine.errors.SignUpError import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Assert.fail @@ -95,7 +91,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { // Initiate challenge, send code to email val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() + (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -108,144 +104,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { assertTrue((submitIncorrectChallengeResult as MFASubmitChallengeError).isInvalidChallenge()) // Request new challenge - val requestNewChallengeResult = sendChallengeResult.nextState.requestChallenge() - assertResult(requestNewChallengeResult) - (requestNewChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(requestNewChallengeResult.sentTo) - assertNotNull(requestNewChallengeResult.codeLength) - assertNotNull(requestNewChallengeResult.channel) - - // Retrieve challenge from mailbox and submit - val otp = tempEmailApi.retrieveCodeFromInbox(username) - val submitCorrectChallengeResult = requestNewChallengeResult.nextState.submitChallenge(otp) - assertResult(submitCorrectChallengeResult) - - val accountState = (submitCorrectChallengeResult as SignInResult.Complete).resultValue - val accessTokenParam = NativeAuthGetAccessTokenParameters() - val getAccessTokenResult = accountState.getAccessToken(accessTokenParam) - assertResult(getAccessTokenResult) - val authResult = (getAccessTokenResult as GetAccessTokenResult.Complete).resultValue - assertTrue(authResult.scope.contains(scopeA)) - assertTrue(authResult.scope.contains(scopeB)) - } - } - } - - /** - * Full flow: - * - Receive MFA required error from API. - * - Request default challenge. - * - Challenge sent successfully, SelectionRequired is returned. - * - Call getAuthMethods to retrieve all auth methods available. - * - Request new challenge. - * - Submit correct challenge. - * - Complete MFA flow and complete sign in. - * - * Note: this test also asserts whether the scopes requested at sign in are present in the token that's received at the end of the flow - */ - @Ignore("Retrieving OTP code failure.") - @Test - fun `test get other auth methods, request challenge on specific auth method and complete MFA flow`() { - config = getConfig(defaultConfigType) - application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) - resources = config.resources - - retryOperation { - runBlocking { - val username = config.email - - val scopeA = resources[0].scopes[0] - val scopeB = resources[0].scopes[1] - - val signInParam = NativeAuthSignInParameters(username = username) - signInParam.password = getSafePassword().toCharArray() - signInParam.scopes = listOf(scopeA, scopeB) - val result = application.signIn(signInParam) - assertResult(result) - - // Initiate challenge, send code to email - val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - // Retrieve other auth methods - val getAuthMethodsResult = sendChallengeResult.nextState.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertTrue(getAuthMethodsResult.authMethods.size == 1) - assertEquals("email", getAuthMethodsResult.authMethods[0].challengeChannel) - - // Request challenge for specific auth method - val requestNewChallengeResult = - sendChallengeResult.nextState.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(requestNewChallengeResult) - (requestNewChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(requestNewChallengeResult.sentTo) - assertNotNull(requestNewChallengeResult.codeLength) - assertNotNull(requestNewChallengeResult.channel) - - // Retrieve challenge from mailbox and submit - val otp = tempEmailApi.retrieveCodeFromInbox(username) - val submitCorrectChallengeResult = requestNewChallengeResult.nextState.submitChallenge(otp) - assertResult(submitCorrectChallengeResult) - - val accountState = (submitCorrectChallengeResult as SignInResult.Complete).resultValue - val accessTokenParam = NativeAuthGetAccessTokenParameters() - val getAccessTokenResult = accountState.getAccessToken(accessTokenParam) - assertResult(getAccessTokenResult) - val authResult = (getAccessTokenResult as GetAccessTokenResult.Complete).resultValue - assertTrue(authResult.scope.contains(scopeA)) - assertTrue(authResult.scope.contains(scopeB)) - } - } - } - - /** - * Full flow: - * - Receive MFA required error from API. - * - Request default challenge. - * - No default auth method available, so SelectionRequired is returned. - * - Request new challenge on specific auth method. - * - Submit correct challenge. - * - Complete MFA flow and complete sign in. - * - * Note: this test also asserts whether the scopes requested at sign in are present in the token that's received at the end of the flow - */ - @Ignore("Retrieving OTP code failure.") - @Test - fun `test selection required, request challenge on specific auth method and complete MFA flow`() { - config = getConfig(ConfigType.SIGN_IN_MFA_MULTI_AUTH) - application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) - resources = config.resources - - retryOperation { - runBlocking { - val username = config.email - - val scopeA = resources[0].scopes[0] - val scopeB = resources[0].scopes[1] - - val param = NativeAuthSignInParameters(username = username) - param.password = getSafePassword().toCharArray() - param.scopes = listOf(scopeA, scopeB) - val result = application.signIn(param) - assertResult(result) - - // Initiate challenge, send code to email - val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.SelectionRequired) - assertTrue(sendChallengeResult.authMethods.size == 1) - assertEquals("email", sendChallengeResult.authMethods[0].challengeChannel) - - // Request challenge for specific auth method - val requestNewChallengeResult = - sendChallengeResult.nextState.requestChallenge(sendChallengeResult.authMethods[0]) + val requestNewChallengeResult = sendChallengeResult.nextState.requestChallenge(result.authMethods.first()) assertResult(requestNewChallengeResult) (requestNewChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(requestNewChallengeResult.sentTo) @@ -301,7 +160,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { // Initiate challenge, send code to email val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() + (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java index df4f4355d8..397becb9c1 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java @@ -49,7 +49,6 @@ import com.microsoft.identity.nativeauth.statemachine.errors.SubmitCodeError; import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult; import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult; -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult; import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult; import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult; import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResendCodeResult; @@ -511,7 +510,7 @@ public void onError(@NonNull BaseException exception) { } @Test - public void testSignInMFAScenario1() throws ExecutionException, InterruptedException, TimeoutException { + public void testSignInMFAScenario2() throws ExecutionException, InterruptedException, TimeoutException { String correlationId = UUID.randomUUID().toString(); configureMockApi( MockApiEndpoint.SignInInitiate, @@ -535,125 +534,12 @@ public void testSignInMFAScenario1() throws ExecutionException, InterruptedExcep MockApiResponseType.MFA_REQUIRED ); - SignInTestCallback signInCallback = new SignInTestCallback(); - - application.signIn( - username, - password, - null, - signInCallback - ); - - SignInResult result = signInCallback.get(); - assertTrue(result instanceof SignInResult.MFARequired); - - correlationId = UUID.randomUUID().toString(); - // 4a. Sign in challenge for default auth method - // 4b. Setup server response with oob required configureMockApi( MockApiEndpoint.SignInChallenge, correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - AwaitingMFAState nextState = spy(((SignInResult.MFARequired) result).getNextState()); - mockCorrelationId(nextState, correlationId); - - AwaitingMFAStateRequestChallengeTestCallback sendChallengeCallback = new AwaitingMFAStateRequestChallengeTestCallback(); - nextState.requestChallenge(sendChallengeCallback); - - MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); - assertTrue(sendChallengeResult instanceof MFARequiredResult.VerificationRequired); - - correlationId = UUID.randomUUID().toString(); - // 5a. Sign in challenge for default auth method - // 5b. Setup server response with introspect required - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, MockApiResponseType.INTROSPECT_SUCCESS ); - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState2 = spy(((MFARequiredResult.VerificationRequired) sendChallengeResult).getNextState()); - mockCorrelationId(nextState2, correlationId); - - GetAuthMethodsTestCallback getAuthMethodsCallback = new GetAuthMethodsTestCallback(); - nextState2.getAuthMethods(getAuthMethodsCallback); - - MFAGetAuthMethodsResult getAuthMethodsResult = getAuthMethodsCallback.get(); - assertTrue(getAuthMethodsResult instanceof MFARequiredResult.SelectionRequired); - - correlationId = UUID.randomUUID().toString(); - // 6a. Sign in challenge for specified auth method - // 6b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState3 = spy(((MFARequiredResult.SelectionRequired) getAuthMethodsResult).getNextState()); - mockCorrelationId(nextState3, correlationId); - - MFARequiredStateRequestChallengeTestCallback sendSelectedAuthMethodCallback = new MFARequiredStateRequestChallengeTestCallback(); - AuthMethod authMethod = ((MFARequiredResult.SelectionRequired) getAuthMethodsResult).getAuthMethods().get(0); - nextState3.requestChallenge(authMethod, sendSelectedAuthMethodCallback); - - MFARequiredResult sendSelectedAuthMethodResult = sendSelectedAuthMethodCallback.get(); - assertTrue(sendSelectedAuthMethodResult instanceof MFARequiredResult.VerificationRequired); - - correlationId = UUID.randomUUID().toString(); - // 7a. Send challenge value to the API - // 7b. Sign in completed, receive tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendSelectedAuthMethodResult).getNextState()); - mockCorrelationId(nextState4, correlationId); - - SubmitChallengeTestCallback submitChallengeCallback = new SubmitChallengeTestCallback(); - nextState4.submitChallenge(code, submitChallengeCallback); - - MFASubmitChallengeResult submitChallengeResult = submitChallengeCallback.get(); - assertTrue(submitChallengeResult instanceof SignInResult.Complete); - } - - @Test - public void testSignInMFAScenario2() throws ExecutionException, InterruptedException, TimeoutException { - String correlationId = UUID.randomUUID().toString(); - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ); - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ); - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ); - SignInTestCallback signInCallback = new SignInTestCallback(); application.signIn( @@ -669,11 +555,7 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep correlationId = UUID.randomUUID().toString(); // 3a. Sign in challenge for default auth method // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.INTROSPECT_REQUIRED - ); + // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. @@ -681,10 +563,11 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep mockCorrelationId(nextState, correlationId); AwaitingMFAStateRequestChallengeTestCallback sendChallengeCallback = new AwaitingMFAStateRequestChallengeTestCallback(); - nextState.requestChallenge(sendChallengeCallback); + AuthMethod authMethod = ((SignInResult.MFARequired) result).getAuthMethods().get(0); + nextState.requestChallenge(authMethod, sendChallengeCallback); MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); - assertTrue(sendChallengeResult instanceof MFARequiredResult.SelectionRequired); + assertTrue(sendChallengeResult instanceof MFARequiredResult.VerificationRequired); correlationId = UUID.randomUUID().toString(); // 6a. Sign in challenge for specified auth method @@ -695,18 +578,6 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep MockApiResponseType.CHALLENGE_TYPE_OOB ); - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState3 = spy(((MFARequiredResult.SelectionRequired) sendChallengeResult).getNextState()); - mockCorrelationId(nextState3, correlationId); - - MFARequiredStateRequestChallengeTestCallback sendSelectedAuthMethodCallback = new MFARequiredStateRequestChallengeTestCallback(); - AuthMethod authMethod = ((MFARequiredResult.SelectionRequired) sendChallengeResult).getAuthMethods().get(0); - nextState3.requestChallenge(authMethod, sendSelectedAuthMethodCallback); - - MFARequiredResult sendSelectedAuthMethodResult = sendSelectedAuthMethodCallback.get(); - assertTrue(sendSelectedAuthMethodResult instanceof MFARequiredResult.VerificationRequired); - correlationId = UUID.randomUUID().toString(); // 7a. Send challenge value to the API // 7b. Sign in completed, receive tokens @@ -718,7 +589,7 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendSelectedAuthMethodResult).getNextState()); + MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendChallengeResult).getNextState()); mockCorrelationId(nextState4, correlationId); SubmitChallengeTestCallback submitChallengeCallback = new SubmitChallengeTestCallback(); @@ -3447,19 +3318,6 @@ public void onError(@NonNull BaseException exception) { } } -class GetAuthMethodsTestCallback extends TestCallback implements MFARequiredState.GetAuthMethodsCallback { - - @Override - public void onResult(MFAGetAuthMethodsResult result) { - future.setResult(result); - } - - @Override - public void onError(@NonNull BaseException exception) { - future.setException(exception); - } -} - class MFARequiredStateRequestChallengeTestCallback extends TestCallback implements MFARequiredState.RequestChallengeCallback { @Override diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt index d48ad0cdb6..5db7ef8ca0 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt @@ -51,15 +51,13 @@ import com.microsoft.identity.nativeauth.parameters.NativeAuthSignUpParameters import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenError import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenErrorTypes -import com.microsoft.identity.nativeauth.statemachine.errors.MFAGetAuthMethodsError -import com.microsoft.identity.nativeauth.statemachine.errors.MFARequestChallengeError +import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordSubmitPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.SignInContinuationError import com.microsoft.identity.nativeauth.statemachine.errors.SignInError import com.microsoft.identity.nativeauth.statemachine.errors.SignUpError import com.microsoft.identity.nativeauth.statemachine.errors.SignUpSubmitAttributesError -import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.SubmitCodeError import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult @@ -2637,212 +2635,6 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) assertTrue((submitCodeResult as SubmitCodeError).error.equals("unsuccessful_command")) // ClientException will be caught in CommandResultUtil.kt and converted to generic error in interface layer } - @Test - fun testSignInMFAVerificationRequiredGetAuthMethodsComplete() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState = spy((result as SignInResult.MFARequired).nextState) - nextState.mockCorrelationId(correlationId) - - // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods - // 4b. Return list of auth methods - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.INTROSPECT_SUCCESS - ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) - - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertNotNull(getAuthMethodsResult.authMethods) - - correlationId = UUID.randomUUID().toString() - // 5a. Sign in challenge for specified auth method - // 5b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(getAuthMethodsResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 6a. Token with oob - // 6b. Success, with tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) - nextState4.mockCorrelationId(correlationId) - - val submitChallengeResult = nextState4.submitChallenge(code) - assertResult(submitChallengeResult) - } - - @Test - fun testSignInMFASelectionRequiredGetAuthMethodsComplete() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - correlationId = UUID.randomUUID().toString() - // 4a. Sign in challenge for default auth method - // 3b. Setup server response with introspect_required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.INTROSPECT_REQUIRED - ) - - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.INTROSPECT_SUCCESS - ) - // Initiate challenge, send code to email - val nextState = spy((result as SignInResult.MFARequired).nextState) - - nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.SelectionRequired) - assertNotNull(sendChallengeResult.authMethods) - - correlationId = UUID.randomUUID().toString() - // 5a. Sign in challenge for specified auth method - // 5b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(sendChallengeResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(sendChallengeResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 6a. Token with oob - // 6b. Success, with tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) - nextState4.mockCorrelationId(correlationId) - - val submitChallengeResult = nextState4.submitChallenge(code) - assertResult(submitChallengeResult) - } - @Test fun testSignInMFAVerificationRequiredComplete() = runTest { // 1. Sign in initiate with username @@ -2888,7 +2680,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -2914,7 +2706,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) } @Test - fun testSignInMFASelectionRequiredGetAuthMethodsRedirect() = runTest { + fun testSignInMFARequiredReturnsEmailAuthMethod() = runTest { // 1. Sign in initiate with username // 1a. Setup server response var correlationId = UUID.randomUUID().toString() @@ -2943,68 +2735,19 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val result = application.signIn(username, password) assertResult(result) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. correlationId = UUID.randomUUID().toString() - // 4a. Sign in challenge for default auth method - // 3b. Setup server response with introspect_required + // 3a. Sign in challenge for default auth method + // 3b. Setup server response with oob required configureMockApi( MockApiEndpoint.SignInChallenge, correlationId, - MockApiResponseType.INTROSPECT_REQUIRED + MockApiResponseType.CHALLENGE_TYPE_OOB ) - + // 3c. Setup oauth/introspect to return email auth method configureMockApi( MockApiEndpoint.Introspect, correlationId, - MockApiResponseType.CHALLENGE_TYPE_REDIRECT - ) - // Initiate challenge, send code to email - val nextState = spy((result as SignInResult.MFARequired).nextState) - - nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - assertTrue((sendChallengeResult as MFARequestChallengeError).isBrowserRequired()) - } - - @Test - fun testSignInMFAVerificationRequiredGetAuthMethodsRedirect() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB + MockApiResponseType.INTROSPECT_SUCCESS ) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock @@ -3013,30 +2756,12 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) nextState.mockCorrelationId(correlationId) // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) assertNotNull(sendChallengeResult.codeLength) assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods - // 4b. Return list of auth methods - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_REDIRECT - ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) - - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - assertTrue((getAuthMethodsResult as MFAGetAuthMethodsError).isBrowserRequired()) } @Test @@ -3084,7 +2809,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -3168,19 +2893,6 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.CHALLENGE_TYPE_OOB ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState = spy((result as SignInResult.MFARequired).nextState) - nextState.mockCorrelationId(correlationId) - - // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - correlationId = UUID.randomUUID().toString() // 4a. Call /introspect to get additional methods // 4b. Return list of auth methods @@ -3189,16 +2901,19 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) correlationId, MockApiResponseType.INTROSPECT_SUCCESS ) + // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) + val nextState = spy((result as SignInResult.MFARequired).nextState) + nextState.mockCorrelationId(correlationId) - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertNotNull(getAuthMethodsResult.authMethods) + // Initiate challenge, send code to email + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) + assertResult(sendChallengeResult) + (sendChallengeResult as MFARequiredResult.VerificationRequired) + assertNotNull(sendChallengeResult.sentTo) + assertNotNull(sendChallengeResult.codeLength) + assertNotNull(sendChallengeResult.channel) correlationId = UUID.randomUUID().toString() // 5a. Sign in challenge for specified auth method @@ -3209,19 +2924,6 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.CHALLENGE_TYPE_OOB ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(getAuthMethodsResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) - correlationId = UUID.randomUUID().toString() // 6a. Token with oob // 6b. Success, with tokens @@ -3233,7 +2935,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) + val nextState4 = spy(sendChallengeResult.nextState) nextState4.mockCorrelationId(correlationId) val submitChallengeResult = nextState4.submitChallenge(code) @@ -3251,7 +2953,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState5 = spy(sendSpecifiedChallengeResult.nextState) + val nextState5 = spy(sendChallengeResult.nextState) nextState4.mockCorrelationId(correlationId) // 8b. Call SDK interface @@ -3305,7 +3007,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -3327,7 +3029,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) nextState2.mockCorrelationId(correlationId) // Resend - val resendChallengeResult = nextState2.requestChallenge() + val resendChallengeResult = nextState2.requestChallenge(result.authMethods.first()) assertResult(resendChallengeResult) (resendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(resendChallengeResult.sentTo) From 220ce0ac5d8452a062187dc6d00b97cf0a367c20 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Fri, 12 Sep 2025 11:04:46 +0100 Subject: [PATCH 04/17] fix tests on client applications --- ...veAuthPublicClientApplicationJavaTest.java | 19 +++++----- ...veAuthPublicClientApplicationKotlinTest.kt | 35 +++++++------------ 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java index 397becb9c1..4886173156 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java @@ -535,7 +535,7 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep ); configureMockApi( - MockApiEndpoint.SignInChallenge, + MockApiEndpoint.Introspect, correlationId, MockApiResponseType.INTROSPECT_SUCCESS ); @@ -556,6 +556,14 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep // 3a. Sign in challenge for default auth method // 3b. Setup server response with oob required + correlationId = UUID.randomUUID().toString(); + // 6a. Sign in challenge for specified auth method + // 6b. Setup server response with oob required + configureMockApi( + MockApiEndpoint.SignInChallenge, + correlationId, + MockApiResponseType.CHALLENGE_TYPE_OOB + ); // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. @@ -569,15 +577,6 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); assertTrue(sendChallengeResult instanceof MFARequiredResult.VerificationRequired); - correlationId = UUID.randomUUID().toString(); - // 6a. Sign in challenge for specified auth method - // 6b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ); - correlationId = UUID.randomUUID().toString(); // 7a. Send challenge value to the API // 7b. Sign in completed, receive tokens diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt index 5db7ef8ca0..a78281b15a 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt @@ -2881,20 +2881,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.MFA_REQUIRED ) - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods + // 4a. Call /introspect to get methods // 4b. Return list of auth methods configureMockApi( MockApiEndpoint.Introspect, @@ -2902,20 +2889,14 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.INTROSPECT_SUCCESS ) + val result = application.signIn(username, password) + assertResult(result) + // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() // 5a. Sign in challenge for specified auth method // 5b. Setup server response with oob required configureMockApi( @@ -2924,6 +2905,14 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.CHALLENGE_TYPE_OOB ) + // Initiate challenge, send code to email + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) + assertResult(sendChallengeResult) + (sendChallengeResult as MFARequiredResult.VerificationRequired) + assertNotNull(sendChallengeResult.sentTo) + assertNotNull(sendChallengeResult.codeLength) + assertNotNull(sendChallengeResult.channel) + correlationId = UUID.randomUUID().toString() // 6a. Token with oob // 6b. Success, with tokens From 3ee685befbcc765bc94f2df4617043f1bcf9ae51 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Fri, 12 Sep 2025 12:00:12 +0100 Subject: [PATCH 05/17] update common --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index 36fb764d37..4683747b5f 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 36fb764d37a78039d3411d0589f8caca222986a8 +Subproject commit 4683747b5f0219d5bd6df74bd5913050b436bbb8 From 7f76fba5774cbf8accf53c1699e5c3d1aea4a23d Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Fri, 12 Sep 2025 14:18:51 +0100 Subject: [PATCH 06/17] update with latest common --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index 4683747b5f..26deb12c26 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 4683747b5f0219d5bd6df74bd5913050b436bbb8 +Subproject commit 26deb12c26bf91916a55be3c5f9fad6b8bfff9e6 From e8a627630784cd05c1fa9ba3942958cd345969d0 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 17 Sep 2025 10:20:28 +0100 Subject: [PATCH 07/17] add mfa and jit results after submitting code and continuation token --- common | 2 +- .../statemachine/results/SignInResult.kt | 4 +-- .../statemachine/states/SignInStates.kt | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/common b/common index 26deb12c26..6d0c7123ad 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 26deb12c26bf91916a55be3c5f9fad6b8bfff9e6 +Subproject commit 6d0c7123ad5e76a7969f5cbab64d71cc531c5ee2 diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt index 2e1e79f9a3..b5de4de4b8 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt @@ -84,7 +84,7 @@ interface SignInResult : Result { class MFARequired( override val nextState: AwaitingMFAState, val authMethods: List - ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult, SignInSubmitCodeResult /** * StrongAuthMethodRegistration Result, which indicates that a registration of a strong authentication method is required to continue. @@ -95,7 +95,7 @@ interface SignInResult : Result { class StrongAuthMethodRegistrationRequired( override val nextState: RegisterStrongAuthState, val authMethods: List - ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult, SignInSubmitCodeResult } /** diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt index ca267ce630..dd93d0e3c6 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt @@ -201,6 +201,27 @@ class SignInCodeRequiredState internal constructor( exception = result.exception ) } + is SignInCommandResult.MFARequired -> { + SignInResult.MFARequired( + nextState = AwaitingMFAState( + continuationToken = result.continuationToken, + correlationId = result.correlationId, + scopes = scopes, + config = config + ), + authMethods = result.authMethods.toListOfAuthMethods() + ) + } + is SignInCommandResult.StrongAuthMethodRegistrationRequired -> { + SignInResult.StrongAuthMethodRegistrationRequired( + nextState = RegisterStrongAuthState( + continuationToken = result.continuationToken, + correlationId = result.correlationId, + config = config + ), + authMethods = result.authMethods.toListOfAuthMethods() + ) + } } } catch (e: Exception) { SubmitCodeError( @@ -720,6 +741,17 @@ class SignInContinuationState( authMethods = result.authMethods.toListOfAuthMethods() ) } + is SignInCommandResult.MFARequired -> { + SignInResult.MFARequired( + nextState = AwaitingMFAState( + continuationToken = result.continuationToken, + correlationId = result.correlationId, + scopes = parameters.scopes, + config = config + ), + authMethods = result.authMethods.toListOfAuthMethods() + ) + } is INativeAuthCommandResult.Redirect -> { SignInContinuationError( errorType = ErrorTypes.BROWSER_REQUIRED, From 2f9d6684b8484e5e36937e34a352d9dc9b15ec28 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 17 Sep 2025 10:46:30 +0100 Subject: [PATCH 08/17] update comment for SMS --- .../parameters/NativeAuthChallengeAuthMethodParameters.kt | 2 +- .../identity/nativeauth/statemachine/results/MFAResult.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt index 9c1418d2be..8efad96dc8 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt @@ -36,7 +36,7 @@ class NativeAuthChallengeAuthMethodParameters( ) { /** - * email to contact to register a new strong authentication method + * email or phone number to contact to register a new strong authentication method */ var verificationContact: String? = null } \ No newline at end of file diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt index f43a36c385..301be512ef 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt @@ -37,7 +37,7 @@ interface MFARequiredResult: Result { * @param nextState [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState] the current state of the flow with follow-on methods. * @param codeLength the length of the challenge required by the server. * @param sentTo the email/phone number the challenge was sent to. - * @param channel the channel(email/phone) the challenge was sent through. + * @param channel the channel(email/sms) the challenge was sent through. */ class VerificationRequired( override val nextState: MFARequiredState, From a836695e2dcbad30eeb7fc891e71c006c845493a Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 17 Sep 2025 14:42:27 +0100 Subject: [PATCH 09/17] check verification is not empty when sms is used --- common | 2 +- .../microsoft/identity/nativeauth/AuthMethod.kt | 4 +--- .../nativeauth/statemachine/states/JITStates.kt | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/common b/common index 6d0c7123ad..9edfe14401 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 6d0c7123ad5e76a7969f5cbab64d71cc531c5ee2 +Subproject commit 9edfe14401b4d1402855a48574c910b7afa137c4 diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt index cac95912ac..5a8a727c61 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt @@ -26,8 +26,6 @@ import android.os.Parcel import android.os.Parcelable import com.microsoft.identity.common.java.nativeauth.providers.responses.signin.AuthenticationMethodApiResult import com.microsoft.identity.common.java.nativeauth.util.ILoggable -import com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState -import com.microsoft.identity.nativeauth.utils.serializable /** * AuthMethod represents a user's authentication methods. @@ -42,7 +40,7 @@ data class AuthMethod( // Auth method login hint (e.g. user@contoso.com) val loginHint: String, - // Auth method challenge channel (email, etc.) + // Auth method challenge channel (email, sms, etc.) val challengeChannel: String, ) : ILoggable, Parcelable { override fun toUnsanitizedString(): String = "AuthMethod(id=$id, " + diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt index 8434b15082..b4a0ba85e2 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt @@ -47,16 +47,23 @@ abstract class BaseJITSubmitChallengeState( tag, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications." ) + // when SMS auth method is used verification contact can't be nil or empty + if (isChallengeChannelSMS(parameters.authMethod.challengeChannel) && parameters.verificationContact.isNullOrBlank()) { + return RegisterStrongAuthChallengeError( + errorType = ErrorTypes.INVALID_INPUT, + errorMessage = "Invalid verification contact", + correlationId = correlationId + ) + } + // if external developer does not provide a verification contact, we use the login hint val verificationContact: String = parameters.verificationContact.takeIf { !it.isNullOrBlank() } ?: parameters.authMethod.loginHint - // Currently, only email is supported for the challengeChannel. Continuation token grant type is used only for "preverified" flow. - val challengeChannel = NativeAuthConstants.ChallengeChannel.EMAIL val params = CommandParametersAdapter.createJITChallengeAuthMethodCommandParameters( config, config.oAuth2TokenCache, verificationContact, - challengeChannel, + parameters.authMethod.challengeChannel, parameters.authMethod.challengeType, correlationId, continuationToken, @@ -134,6 +141,10 @@ abstract class BaseJITSubmitChallengeState( } } } + + private fun isChallengeChannelSMS(challengeChannel: String): Boolean { + return challengeChannel.equals(NativeAuthConstants.ChallengeChannel.SMS, ignoreCase = true) + } } class RegisterStrongAuthState( From 9d38e7920993e9b02f1e027ce699ece209d30ebe Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 17 Sep 2025 16:00:18 +0100 Subject: [PATCH 10/17] add new custom error for verification contact blocked --- common | 2 +- .../nativeauth/statemachine/errors/Error.kt | 15 ++++++--------- .../nativeauth/statemachine/errors/JITErrors.kt | 6 ++++++ .../nativeauth/statemachine/states/JITStates.kt | 9 +++++++++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/common b/common index 9edfe14401..ab000246d1 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 9edfe14401b4d1402855a48574c910b7afa137c4 +Subproject commit ab000246d1e00bf4be351877ec0b13518a5f811d diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/Error.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/Error.kt index c93dc23d19..91c5456af8 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/Error.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/Error.kt @@ -23,23 +23,14 @@ package com.microsoft.identity.nativeauth.statemachine.errors -import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResendCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResult -import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordStartResult import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordSubmitCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordSubmitPasswordResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResendCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.SignInResult import com.microsoft.identity.nativeauth.statemachine.results.SignInSubmitCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.SignInSubmitPasswordResult import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult import com.microsoft.identity.nativeauth.statemachine.results.SignUpResendCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult -import com.microsoft.identity.nativeauth.statemachine.results.SignUpSubmitAttributesResult import com.microsoft.identity.nativeauth.statemachine.results.SignUpSubmitCodeResult -import com.microsoft.identity.nativeauth.statemachine.results.SignUpSubmitPasswordResult /** * ErrorTypes class holds the possible error type values that are shared between the errors @@ -89,6 +80,12 @@ internal class ErrorTypes { */ const val INVALID_INPUT = "invalid_input" + /* + * The VERIFICATION_CONTACT_BLOCKED value indicates the verification contact provided has been blocked. + * Try using another email or phone number, or select an alternative authentication method. + */ + const val VERIFICATION_CONTACT_BLOCKED = "verification_contact_blocked" + /* * The INVALID_STATE value indicates a misconfigured or expired state, or an internal error * in state transitions. If this occurs, the flow should be restarted. diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/JITErrors.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/JITErrors.kt index a3c31348f7..e0c5e59eed 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/JITErrors.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/JITErrors.kt @@ -13,6 +13,12 @@ class RegisterStrongAuthChallengeError( ): BrowserRequiredError, RegisterStrongAuthChallengeResult, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) { fun isInvalidInput(): Boolean = this.errorType == ErrorTypes.INVALID_INPUT + + /* + * Returns true if the verification contact provided has been blocked. + * Try using another email or phone number, or select an alternative authentication method. + */ + fun isVerificationContactBlocked(): Boolean = this.errorType == ErrorTypes.VERIFICATION_CONTACT_BLOCKED } class RegisterStrongAuthSubmitChallengeError( diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt index b4a0ba85e2..ffab93752c 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt @@ -115,6 +115,15 @@ abstract class BaseJITSubmitChallengeState( errorCodes = result.errorCodes ) } + is JITCommandResult.BlockedVerificationContact -> { + RegisterStrongAuthChallengeError( + errorType = ErrorTypes.VERIFICATION_CONTACT_BLOCKED, + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + } is JITCommandResult.VerificationRequired -> { RegisterStrongAuthChallengeResult.VerificationRequired( result = NativeAuthRegisterStrongAuthVerificationRequiredResultParameter( From 424f24d6aa2b9cc19a73f5d1447532a86d55e559 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 17 Sep 2025 16:27:56 +0100 Subject: [PATCH 11/17] make login hint optional, use username instead of loginHint --- common | 2 +- .../microsoft/identity/nativeauth/AuthMethod.kt | 2 +- .../NativeAuthPublicClientApplication.kt | 3 +++ .../nativeauth/statemachine/states/JITStates.kt | 14 ++++++++++---- .../nativeauth/statemachine/states/SignInStates.kt | 8 ++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/common b/common index ab000246d1..9dfc3333a3 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit ab000246d1e00bf4be351877ec0b13518a5f811d +Subproject commit 9dfc3333a3807f86dd812bccdc462d08c68e763c diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt index 5a8a727c61..2c8f31c40c 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/AuthMethod.kt @@ -38,7 +38,7 @@ data class AuthMethod( val challengeType: String, // Auth method login hint (e.g. user@contoso.com) - val loginHint: String, + val loginHint: String?, // Auth method challenge channel (email, sms, etc.) val challengeChannel: String, diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt index 497a72039b..a829b3fc21 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt @@ -682,6 +682,7 @@ class NativeAuthPublicClientApplication( nextState = SignInCodeRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, scopes = scopes, config = nativeAuthConfig, claimsRequestJson = params.claimsRequestJson @@ -719,6 +720,7 @@ class NativeAuthPublicClientApplication( nextState = SignInPasswordRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, scopes = scopes, config = nativeAuthConfig, claimsRequestJson = params.claimsRequestJson @@ -778,6 +780,7 @@ class NativeAuthPublicClientApplication( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, config = nativeAuthConfig ), authMethods = result.authMethods.toListOfAuthMethods() diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt index ffab93752c..6fa6324c61 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.withContext abstract class BaseJITSubmitChallengeState( override val continuationToken: String, override val correlationId: String, + internal open val username: String, private val config: NativeAuthPublicClientApplicationConfiguration ) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable { suspend fun internalChallengeAuthMethod(parameters: NativeAuthChallengeAuthMethodParameters, tag: String): RegisterStrongAuthChallengeResult { @@ -56,8 +57,8 @@ abstract class BaseJITSubmitChallengeState( ) } - // if external developer does not provide a verification contact, we use the login hint - val verificationContact: String = parameters.verificationContact.takeIf { !it.isNullOrBlank() } ?: parameters.authMethod.loginHint + // if external developer does not provide a verification contact, we use the username + val verificationContact: String = parameters.verificationContact.takeIf { !it.isNullOrBlank() } ?: username val params = CommandParametersAdapter.createJITChallengeAuthMethodCommandParameters( config, @@ -130,6 +131,7 @@ abstract class BaseJITSubmitChallengeState( nextState = RegisterStrongAuthVerificationRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, config = config ), codeLength = result.codeLength, @@ -159,8 +161,9 @@ abstract class BaseJITSubmitChallengeState( class RegisterStrongAuthState( override val continuationToken: String, override val correlationId: String, + override val username: String, private val config: NativeAuthPublicClientApplicationConfiguration -) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config), State, Parcelable { +) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config, username = username), State, Parcelable { private val TAG: String = RegisterStrongAuthState::class.java.simpleName /** @@ -213,6 +216,7 @@ class RegisterStrongAuthState( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", + username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration ) @@ -241,8 +245,9 @@ class RegisterStrongAuthState( class RegisterStrongAuthVerificationRequiredState( override val continuationToken: String, override val correlationId: String, + override val username: String, private val config: NativeAuthPublicClientApplicationConfiguration -) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config) { +) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, username = username, config = config) { private val TAG: String = RegisterStrongAuthVerificationRequiredState::class.java.simpleName @@ -422,6 +427,7 @@ class RegisterStrongAuthVerificationRequiredState( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", + username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration ) diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt index dd93d0e3c6..18ef2252ca 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt @@ -76,6 +76,7 @@ import kotlinx.coroutines.withContext class SignInCodeRequiredState internal constructor( override val continuationToken: String, override val correlationId: String, + private val username: String, private val scopes: List?, private val claimsRequestJson: String?, private val config: NativeAuthPublicClientApplicationConfiguration @@ -84,6 +85,7 @@ class SignInCodeRequiredState internal constructor( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", + username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), @@ -217,6 +219,7 @@ class SignInCodeRequiredState internal constructor( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() @@ -298,6 +301,7 @@ class SignInCodeRequiredState internal constructor( nextState = SignInCodeRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, scopes = scopes, config = config, claimsRequestJson = claimsRequestJson @@ -379,6 +383,7 @@ class SignInCodeRequiredState internal constructor( class SignInPasswordRequiredState( override val continuationToken: String, override val correlationId: String, + private val username: String, private val scopes: List?, private val claimsRequestJson: String?, private val config: NativeAuthPublicClientApplicationConfiguration @@ -386,6 +391,7 @@ class SignInPasswordRequiredState( private val TAG: String = SignInPasswordRequiredState::class.java.simpleName constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", + username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), @@ -482,6 +488,7 @@ class SignInPasswordRequiredState( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() @@ -736,6 +743,7 @@ class SignInContinuationState( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() From 1f9b11af7848da0b50c740b68e31b6c995680b95 Mon Sep 17 00:00:00 2001 From: Danilo Raspa <105228698+nilo-ms@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:48:45 +0100 Subject: [PATCH 12/17] Native auth: Update Email OTP MFA to Match EC Implementation, Fixes AB#3351233 (#2379) This PR updates the SDK to match the latest flow from EC. In this new flow, the developer must always supply an auth Method to the /oauth2/v2.0/challenge endpoint which means once the .mfaRequired error is received from token endpoint, the /oauth2/v2.0/introspect endpoint needs to be called to retrieve the methods which are automatically returned to the external developer. Furthermore, whenever calling the /token endpoint is called with an MFA Email OTP code, the grant type should be mfa_oob Fixes [AB#3351233](https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3351233) MSAL Common PR: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/pull/2760 --- common | 2 +- .../internal/CommandParametersAdapter.java | 96 +---- .../NativeAuthPublicClientApplication.kt | 3 +- .../statemachine/errors/MFAErrors.kt | 22 -- .../statemachine/results/MFAResult.kt | 19 - .../statemachine/results/SignInResult.kt | 7 +- .../statemachine/states/MFAStates.kt | 192 +-------- .../statemachine/states/SignInStates.kt | 3 +- .../tests/network/nativeauth/SignInMFATest.kt | 147 +------ ...veAuthPublicClientApplicationJavaTest.java | 161 +------- ...veAuthPublicClientApplicationKotlinTest.kt | 365 ++---------------- 11 files changed, 74 insertions(+), 943 deletions(-) diff --git a/common b/common index 0fe896cb11..26deb12c26 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 0fe896cb11a671bcc1a0228c2a3d69bca0c21521 +Subproject commit 26deb12c26bf91916a55be3c5f9fad6b8bfff9e6 diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index 28d41e459f..24bad0f4d1 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -61,9 +61,7 @@ import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.GetAuthMethodsCommandParameters; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFADefaultChallengeCommandParameters; -import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFASelectedDefaultChallengeCommandParameters; +import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFAChallengeAuthMethodCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.MFASubmitChallengeCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordResendCodeCommandParameters; import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordStartCommandParameters; @@ -778,54 +776,6 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo return commandParameters; } - /** - * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand}] of Native Auth - * @param configuration PCA configuration - * @param tokenCache token cache for storing results - * @param correlationId correlation ID to use in the API request, taken from the previous request in the flow - * @param continuationToken continuation token - * @param scopes scopes requested during sign in flow - * @return Command parameter object - * @throws ClientException - */ - public static MFADefaultChallengeCommandParameters createMFADefaultChallengeCommandParameters( - @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, - @NonNull final OAuth2TokenCache tokenCache, - @NonNull final String continuationToken, - @NonNull final String correlationId, - final List scopes) throws ClientException { - - final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - - final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( - AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), - null - ); - - final MFADefaultChallengeCommandParameters commandParameters = - MFADefaultChallengeCommandParameters.builder() - .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) - .applicationName(configuration.getAppContext().getPackageName()) - .applicationVersion(getPackageVersion(configuration.getAppContext())) - .clientId(configuration.getClientId()) - .isSharedDevice(configuration.getIsSharedDevice()) - .redirectUri(configuration.getRedirectUri()) - .oAuth2TokenCache(tokenCache) - .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) - .sdkType(SdkType.MSAL) - .sdkVersion(PublicClientApplication.getSdkVersion()) - .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) - .authority(authority) - .authenticationScheme(authenticationScheme) - .continuationToken(continuationToken) - .scopes(scopes) - .challengeType(configuration.getChallengeTypes()) - .correlationId(correlationId) - .build(); - - return commandParameters; - } - /** * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand}] of Native Auth * @param configuration PCA configuration @@ -836,7 +786,7 @@ public static MFADefaultChallengeCommandParameters createMFADefaultChallengeComm * @return Command parameter object * @throws ClientException */ - public static MFASelectedDefaultChallengeCommandParameters createMFASelectedChallengeCommandParameters( + public static MFAChallengeAuthMethodCommandParameters createMFAChallengeAuthMethodCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @NonNull final String continuationToken, @@ -853,8 +803,8 @@ public static MFASelectedDefaultChallengeCommandParameters createMFASelectedChal final String authMethodId = authMethod.getId(); - final MFASelectedDefaultChallengeCommandParameters commandParameters = - MFASelectedDefaultChallengeCommandParameters.builder() + final MFAChallengeAuthMethodCommandParameters commandParameters = + MFAChallengeAuthMethodCommandParameters.builder() .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) .applicationName(configuration.getAppContext().getPackageName()) .applicationVersion(getPackageVersion(configuration.getAppContext())) @@ -927,44 +877,6 @@ public static MFASubmitChallengeCommandParameters createMFASubmitChallengeComman return commandParameters; } - /** - * Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.GetAuthMethodsCommand}] of Native Auth - * @param configuration PCA configuration - * @param tokenCache token cache for storing results - * @param correlationId correlation ID to use in the API request, taken from the previous request in the flow - * @param continuationToken Continuation token - * @return Command parameter object - */ - public static GetAuthMethodsCommandParameters createGetAuthMethodsCommandParameters( - @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, - @NonNull final OAuth2TokenCache tokenCache, - @NonNull final String continuationToken, - @NonNull final String correlationId) { - - final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - - final GetAuthMethodsCommandParameters commandParameters = - GetAuthMethodsCommandParameters.builder() - .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) - .applicationName(configuration.getAppContext().getPackageName()) - .applicationVersion(getPackageVersion(configuration.getAppContext())) - .clientId(configuration.getClientId()) - .isSharedDevice(configuration.getIsSharedDevice()) - .redirectUri(configuration.getRedirectUri()) - .oAuth2TokenCache(tokenCache) - .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) - .sdkType(SdkType.MSAL) - .sdkVersion(PublicClientApplication.getSdkVersion()) - .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) - .authority(authority) - .continuationToken(continuationToken) - .challengeType(configuration.getChallengeTypes()) - .correlationId(correlationId) - .build(); - - return commandParameters; - } - /** * Creates command parameter for [ResetPasswordStartCommand] of Native Auth. * @param configuration PCA configuration diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt index 45cfe98667..497a72039b 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt @@ -768,7 +768,8 @@ class NativeAuthPublicClientApplication( correlationId = result.correlationId, scopes = scopes, config = nativeAuthConfig - ) + ), + authMethods = result.authMethods.toListOfAuthMethods() ) } diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt index b6c6a583f6..27ffe9b2bf 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/MFAErrors.kt @@ -1,6 +1,5 @@ package com.microsoft.identity.nativeauth.statemachine.errors -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult @@ -26,27 +25,6 @@ class MFARequestChallengeError( override var exception: Exception? = null ): MFARequiredResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) -/** - * MFA get authentication methods error. Use the utility methods of this class - * to identify and handle the error. This error is produced by - * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods] - * @param errorType the error type value of the error that occurred. - * @param error the error returned by the authentication server. - * @param errorMessage the error message returned by the authentication server. - * @param correlationId a unique identifier for the request that can help in diagnostics. - * @param errorCodes a list of specific error codes returned by the authentication server. - * @param exception an internal unexpected exception that happened. - */ -class MFAGetAuthMethodsError( - override val errorType: String? = null, - override val error: String? = null, - override val errorMessage: String?, - override val correlationId: String, - override val errorCodes: List? = null, - val subError: String? = null, - override var exception: Exception? = null -): MFAGetAuthMethodsResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) - /** * MFA submit challenge error. The user should use the utility methods of this class * to identify and handle the error. This error is produced by diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt index 6c87895b62..f43a36c385 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt @@ -23,7 +23,6 @@ package com.microsoft.identity.nativeauth.statemachine.results -import com.microsoft.identity.nativeauth.AuthMethod import com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState /** @@ -46,26 +45,8 @@ interface MFARequiredResult: Result { val sentTo: String, val channel: String, ) : MFARequiredResult, Result.SuccessResult(nextState = nextState) - - /** - * Selection required result, which indicates that a specific authentication method must be selected, which - * the server will send the challenge to (once sendChallenge() is called). - * - * @param nextState [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState] the current state of the flow with follow-on methods. - * @param authMethods the authentication methods that can be used to complete the challenge flow. - */ - class SelectionRequired( - override val nextState: MFARequiredState, - val authMethods: List - ) : MFARequiredResult, MFAGetAuthMethodsResult, Result.SuccessResult(nextState = nextState) } -/** - * Results related to get authentication methods operation, produced by - * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods] - */ -interface MFAGetAuthMethodsResult : Result - /** * Results related to MFA submit challenge operation, produced by * [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.submitChallenge] diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt index 9c32fd6751..2e1e79f9a3 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt @@ -82,8 +82,9 @@ interface SignInResult : Result { * @param nextState [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState] the current state of the flow with follow-on methods. */ class MFARequired( - override val nextState: AwaitingMFAState - ) : SignInResult, Result.SuccessResult(nextState = nextState), SignInSubmitPasswordResult + override val nextState: AwaitingMFAState, + val authMethods: List + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult /** * StrongAuthMethodRegistration Result, which indicates that a registration of a strong authentication method is required to continue. @@ -94,7 +95,7 @@ interface SignInResult : Result { class StrongAuthMethodRegistrationRequired( override val nextState: RegisterStrongAuthState, val authMethods: List - ) : SignInResult, SignInSubmitPasswordResult, Result.SuccessResult(nextState = nextState) + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult } /** diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt index 2dbcaf149a..d4f8401677 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/MFAStates.kt @@ -32,14 +32,12 @@ import com.microsoft.identity.common.java.controllers.CommandDispatcher import com.microsoft.identity.common.java.eststelemetry.PublicApiId import com.microsoft.identity.common.java.logging.LogSession import com.microsoft.identity.common.java.logging.Logger -import com.microsoft.identity.common.java.nativeauth.controllers.results.GetAuthMethodsCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.INativeAuthCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFAChallengeCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFACommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.MFASubmitChallengeCommandResult import com.microsoft.identity.common.java.nativeauth.controllers.results.SignInCommandResult import com.microsoft.identity.common.java.nativeauth.util.checkAndWrapCommandResultType -import com.microsoft.identity.common.nativeauth.internal.commands.GetAuthMethodsCommand import com.microsoft.identity.common.nativeauth.internal.commands.MFAChallengeCommand import com.microsoft.identity.common.nativeauth.internal.commands.MFASubmitChallengeCommand import com.microsoft.identity.common.nativeauth.internal.controllers.NativeAuthMsalController @@ -47,14 +45,11 @@ import com.microsoft.identity.nativeauth.AuthMethod import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes -import com.microsoft.identity.nativeauth.statemachine.errors.MFAGetAuthMethodsError import com.microsoft.identity.nativeauth.statemachine.errors.MFARequestChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult -import com.microsoft.identity.nativeauth.toListOfAuthMethods import com.microsoft.identity.nativeauth.utils.serializable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -77,18 +72,19 @@ class AwaitingMFAState( * Requests a challenge to be sent to the user's default authentication method; callback variant. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. + * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. * @param callback [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState.RequestChallengeCallback] to receive the result on. * @return The result of the request challenge action. */ - fun requestChallenge(callback: RequestChallengeCallback) { + fun requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback) { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge(callback: RequestChallengeCallback)" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback)" ) NativeAuthPublicClientApplication.pcaScope.launch { try { - val result = requestChallenge() + val result = requestChallenge(authMethod) callback.onResult(result) } catch (e: MsalException) { Logger.error(TAG, "Exception thrown in requestChallenge", e) @@ -103,28 +99,28 @@ class AwaitingMFAState( * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @return The result of the request challenge action. */ - suspend fun requestChallenge(): MFARequiredResult { + suspend fun requestChallenge(authMethod: AuthMethod): MFARequiredResult { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge()" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod)" ) Logger.warn(TAG, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications.") return withContext(Dispatchers.IO) { try { - val params = CommandParametersAdapter.createMFADefaultChallengeCommandParameters( + val params = CommandParametersAdapter.createMFAChallengeAuthMethodCommandParameters( config, config.oAuth2TokenCache, continuationToken, correlationId, - scopes + authMethod ) val command = MFAChallengeCommand( parameters = params, controller = NativeAuthMsalController(), - publicApiId = PublicApiId.NATIVE_AUTH_MFA_DEFAULT_CHALLENGE + publicApiId = PublicApiId.NATIVE_AUTH_MFA_SELECTED_CHALLENGE ) val rawCommandResult = @@ -146,22 +142,11 @@ class AwaitingMFAState( channel = result.challengeChannel ) } - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } is INativeAuthCommandResult.APIError -> { Logger.warnWithObject( TAG, result.correlationId, - "requestChallenge() received unexpected result: ", + "requestChallenge(authMethod: AuthMethod) received unexpected result: ", result ) MFARequestChallengeError( @@ -229,116 +214,6 @@ class MFARequiredState( ) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable { private val TAG: String = MFARequiredState::class.java.simpleName - /** - * GetAuthMethodsCallback receives the result for getAuthMethods() in MFA flows in native authentication. - */ - interface GetAuthMethodsCallback : Callback - - /** - * Retrieves all authentication methods that can be used to complete the challenge flow; callback variant. - * - * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. - * @param callback [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.GetAuthMethodsCallback] to receive the result on. - * @return The results of the get authentication methods action. - */ - fun getAuthMethods(callback: GetAuthMethodsCallback) { - LogSession.logMethodCall( - tag = TAG, - correlationId = correlationId, - methodName = "${TAG}.getAuthMethods(callback: GetAuthMethodsCallback)" - ) - NativeAuthPublicClientApplication.pcaScope.launch { - try { - val result = getAuthMethods() - callback.onResult(result) - } catch (e: MsalException) { - Logger.error(TAG, "Exception thrown in getAuthMethods", e) - callback.onError(e) - } - } - } - - /** - * Retrieves all authentication methods that can be used to complete the challenge flow; Kotlin coroutines variant. - * - * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. - * @return The results of the get authentication methods action. - */ - suspend fun getAuthMethods(): MFAGetAuthMethodsResult { - LogSession.logMethodCall( - tag = TAG, - correlationId = correlationId, - methodName = "${TAG}.getAuthMethods()" - ) - - Logger.warn(TAG, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications.") - - return withContext(Dispatchers.IO) { - try { - val params = CommandParametersAdapter.createGetAuthMethodsCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId - ) - val command = GetAuthMethodsCommand( - parameters = params, - controller = NativeAuthMsalController(), - publicApiId = PublicApiId.NATIVE_AUTH_GET_AUTH_METHODS - ) - - val rawCommandResult = - CommandDispatcher.submitSilentReturningFuture(command) - .get() - - return@withContext when (val result = - rawCommandResult.checkAndWrapCommandResultType()) { - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } - is INativeAuthCommandResult.APIError -> { - Logger.warnWithObject( - TAG, - result.correlationId, - "getAuthMethods() received unexpected result: ", - result - ) - MFAGetAuthMethodsError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - errorCodes = result.errorCodes, - exception = result.exception - ) - } - is INativeAuthCommandResult.Redirect -> { - MFAGetAuthMethodsError( - errorType = ErrorTypes.BROWSER_REQUIRED, - error = result.error, - errorMessage = result.redirectReason, - correlationId = result.correlationId - ) - } - } - } catch (e: Exception) { - MFAGetAuthMethodsError( - errorType = ErrorTypes.CLIENT_EXCEPTION, - errorMessage = "MSAL client exception occurred in getAuthMethods().", - exception = e, - correlationId = correlationId - ) - } - } - } - /** * RequestChallengeCallback receives the result for requestChallenge() in MFA flows in native authentication. */ @@ -346,19 +221,17 @@ class MFARequiredState( /** * Requests a challenge to be sent to the user's default authentication method; callback variant. - * If an authentication method ID was supplied, the server will send a challenge to the specified method. If no ID is supplied, - * the server will attempt to send the challenge to the user's default auth method. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. - * @param callback [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.RequestChallengeCallback] to receive the result on. + * @param callback [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState.RequestChallengeCallback] to receive the result on. * @return The result of the request challenge action. */ - fun requestChallenge(authMethod: AuthMethod? = null, callback: RequestChallengeCallback) { + fun requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback) { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, - methodName = "${TAG}.requestChallenge(callback: RequestChallengeCallback)" + methodName = "${TAG}.requestChallenge(authMethod: AuthMethod, callback: RequestChallengeCallback)" ) NativeAuthPublicClientApplication.pcaScope.launch { try { @@ -373,14 +246,12 @@ class MFARequiredState( /** * Requests a challenge to be sent to the user's default authentication method; Kotlin coroutines variant. - * If an authentication method ID was supplied, the server will send a challenge to the specified method. If no ID is supplied, - * the server will attempt to send the challenge to the user's default auth method. * * Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications. * @param authMethod [com.microsoft.identity.nativeauth.AuthMethod] the authentication method used for the challenge operation. * @return The result of the request challenge action. */ - suspend fun requestChallenge(authMethod: AuthMethod? = null): MFARequiredResult { + suspend fun requestChallenge(authMethod: AuthMethod): MFARequiredResult { LogSession.logMethodCall( tag = TAG, correlationId = correlationId, @@ -391,23 +262,13 @@ class MFARequiredState( return withContext(Dispatchers.IO) { try { - val params = if (authMethod != null) { - CommandParametersAdapter.createMFASelectedChallengeCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId, - authMethod - ) - } else { - CommandParametersAdapter.createMFADefaultChallengeCommandParameters( - config, - config.oAuth2TokenCache, - continuationToken, - correlationId, - scopes - ) - } + val params = CommandParametersAdapter.createMFAChallengeAuthMethodCommandParameters( + config, + config.oAuth2TokenCache, + continuationToken, + correlationId, + authMethod + ) val command = MFAChallengeCommand( parameters = params, @@ -434,17 +295,6 @@ class MFARequiredState( channel = result.challengeChannel ) } - is MFACommandResult.SelectionRequired -> { - MFARequiredResult.SelectionRequired( - nextState = MFARequiredState( - continuationToken = result.continuationToken, - correlationId = result.correlationId, - scopes = scopes, - config = config - ), - authMethods = result.authMethods.toListOfAuthMethods() - ) - } is INativeAuthCommandResult.APIError -> { Logger.warnWithObject( TAG, diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt index 8b74cc60b2..ca267ce630 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt @@ -452,7 +452,8 @@ class SignInPasswordRequiredState( correlationId = result.correlationId, scopes = scopes, config = config - ) + ), + authMethods = result.authMethods.toListOfAuthMethods() ) } is SignInCommandResult.StrongAuthMethodRegistrationRequired -> { diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt index 8847577d2a..fdb0d595b4 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt @@ -32,15 +32,11 @@ import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthGetAccessTokenParameters import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInParameters import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError -import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.SignInError -import com.microsoft.identity.nativeauth.statemachine.errors.SignUpError import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Assert.fail @@ -95,7 +91,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { // Initiate challenge, send code to email val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() + (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -108,144 +104,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { assertTrue((submitIncorrectChallengeResult as MFASubmitChallengeError).isInvalidChallenge()) // Request new challenge - val requestNewChallengeResult = sendChallengeResult.nextState.requestChallenge() - assertResult(requestNewChallengeResult) - (requestNewChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(requestNewChallengeResult.sentTo) - assertNotNull(requestNewChallengeResult.codeLength) - assertNotNull(requestNewChallengeResult.channel) - - // Retrieve challenge from mailbox and submit - val otp = tempEmailApi.retrieveCodeFromInbox(username) - val submitCorrectChallengeResult = requestNewChallengeResult.nextState.submitChallenge(otp) - assertResult(submitCorrectChallengeResult) - - val accountState = (submitCorrectChallengeResult as SignInResult.Complete).resultValue - val accessTokenParam = NativeAuthGetAccessTokenParameters() - val getAccessTokenResult = accountState.getAccessToken(accessTokenParam) - assertResult(getAccessTokenResult) - val authResult = (getAccessTokenResult as GetAccessTokenResult.Complete).resultValue - assertTrue(authResult.scope.contains(scopeA)) - assertTrue(authResult.scope.contains(scopeB)) - } - } - } - - /** - * Full flow: - * - Receive MFA required error from API. - * - Request default challenge. - * - Challenge sent successfully, SelectionRequired is returned. - * - Call getAuthMethods to retrieve all auth methods available. - * - Request new challenge. - * - Submit correct challenge. - * - Complete MFA flow and complete sign in. - * - * Note: this test also asserts whether the scopes requested at sign in are present in the token that's received at the end of the flow - */ - @Ignore("Retrieving OTP code failure.") - @Test - fun `test get other auth methods, request challenge on specific auth method and complete MFA flow`() { - config = getConfig(defaultConfigType) - application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) - resources = config.resources - - retryOperation { - runBlocking { - val username = config.email - - val scopeA = resources[0].scopes[0] - val scopeB = resources[0].scopes[1] - - val signInParam = NativeAuthSignInParameters(username = username) - signInParam.password = getSafePassword().toCharArray() - signInParam.scopes = listOf(scopeA, scopeB) - val result = application.signIn(signInParam) - assertResult(result) - - // Initiate challenge, send code to email - val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - // Retrieve other auth methods - val getAuthMethodsResult = sendChallengeResult.nextState.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertTrue(getAuthMethodsResult.authMethods.size == 1) - assertEquals("email", getAuthMethodsResult.authMethods[0].challengeChannel) - - // Request challenge for specific auth method - val requestNewChallengeResult = - sendChallengeResult.nextState.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(requestNewChallengeResult) - (requestNewChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(requestNewChallengeResult.sentTo) - assertNotNull(requestNewChallengeResult.codeLength) - assertNotNull(requestNewChallengeResult.channel) - - // Retrieve challenge from mailbox and submit - val otp = tempEmailApi.retrieveCodeFromInbox(username) - val submitCorrectChallengeResult = requestNewChallengeResult.nextState.submitChallenge(otp) - assertResult(submitCorrectChallengeResult) - - val accountState = (submitCorrectChallengeResult as SignInResult.Complete).resultValue - val accessTokenParam = NativeAuthGetAccessTokenParameters() - val getAccessTokenResult = accountState.getAccessToken(accessTokenParam) - assertResult(getAccessTokenResult) - val authResult = (getAccessTokenResult as GetAccessTokenResult.Complete).resultValue - assertTrue(authResult.scope.contains(scopeA)) - assertTrue(authResult.scope.contains(scopeB)) - } - } - } - - /** - * Full flow: - * - Receive MFA required error from API. - * - Request default challenge. - * - No default auth method available, so SelectionRequired is returned. - * - Request new challenge on specific auth method. - * - Submit correct challenge. - * - Complete MFA flow and complete sign in. - * - * Note: this test also asserts whether the scopes requested at sign in are present in the token that's received at the end of the flow - */ - @Ignore("Retrieving OTP code failure.") - @Test - fun `test selection required, request challenge on specific auth method and complete MFA flow`() { - config = getConfig(ConfigType.SIGN_IN_MFA_MULTI_AUTH) - application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) - resources = config.resources - - retryOperation { - runBlocking { - val username = config.email - - val scopeA = resources[0].scopes[0] - val scopeB = resources[0].scopes[1] - - val param = NativeAuthSignInParameters(username = username) - param.password = getSafePassword().toCharArray() - param.scopes = listOf(scopeA, scopeB) - val result = application.signIn(param) - assertResult(result) - - // Initiate challenge, send code to email - val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.SelectionRequired) - assertTrue(sendChallengeResult.authMethods.size == 1) - assertEquals("email", sendChallengeResult.authMethods[0].challengeChannel) - - // Request challenge for specific auth method - val requestNewChallengeResult = - sendChallengeResult.nextState.requestChallenge(sendChallengeResult.authMethods[0]) + val requestNewChallengeResult = sendChallengeResult.nextState.requestChallenge(result.authMethods.first()) assertResult(requestNewChallengeResult) (requestNewChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(requestNewChallengeResult.sentTo) @@ -301,7 +160,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { // Initiate challenge, send code to email val sendChallengeResult = - (result as SignInResult.MFARequired).nextState.requestChallenge() + (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java index df4f4355d8..4886173156 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java @@ -49,7 +49,6 @@ import com.microsoft.identity.nativeauth.statemachine.errors.SubmitCodeError; import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult; import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult; -import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult; import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult; import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult; import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResendCodeResult; @@ -511,7 +510,7 @@ public void onError(@NonNull BaseException exception) { } @Test - public void testSignInMFAScenario1() throws ExecutionException, InterruptedException, TimeoutException { + public void testSignInMFAScenario2() throws ExecutionException, InterruptedException, TimeoutException { String correlationId = UUID.randomUUID().toString(); configureMockApi( MockApiEndpoint.SignInInitiate, @@ -535,125 +534,12 @@ public void testSignInMFAScenario1() throws ExecutionException, InterruptedExcep MockApiResponseType.MFA_REQUIRED ); - SignInTestCallback signInCallback = new SignInTestCallback(); - - application.signIn( - username, - password, - null, - signInCallback - ); - - SignInResult result = signInCallback.get(); - assertTrue(result instanceof SignInResult.MFARequired); - - correlationId = UUID.randomUUID().toString(); - // 4a. Sign in challenge for default auth method - // 4b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - AwaitingMFAState nextState = spy(((SignInResult.MFARequired) result).getNextState()); - mockCorrelationId(nextState, correlationId); - - AwaitingMFAStateRequestChallengeTestCallback sendChallengeCallback = new AwaitingMFAStateRequestChallengeTestCallback(); - nextState.requestChallenge(sendChallengeCallback); - - MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); - assertTrue(sendChallengeResult instanceof MFARequiredResult.VerificationRequired); - - correlationId = UUID.randomUUID().toString(); - // 5a. Sign in challenge for default auth method - // 5b. Setup server response with introspect required configureMockApi( MockApiEndpoint.Introspect, correlationId, MockApiResponseType.INTROSPECT_SUCCESS ); - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState2 = spy(((MFARequiredResult.VerificationRequired) sendChallengeResult).getNextState()); - mockCorrelationId(nextState2, correlationId); - - GetAuthMethodsTestCallback getAuthMethodsCallback = new GetAuthMethodsTestCallback(); - nextState2.getAuthMethods(getAuthMethodsCallback); - - MFAGetAuthMethodsResult getAuthMethodsResult = getAuthMethodsCallback.get(); - assertTrue(getAuthMethodsResult instanceof MFARequiredResult.SelectionRequired); - - correlationId = UUID.randomUUID().toString(); - // 6a. Sign in challenge for specified auth method - // 6b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState3 = spy(((MFARequiredResult.SelectionRequired) getAuthMethodsResult).getNextState()); - mockCorrelationId(nextState3, correlationId); - - MFARequiredStateRequestChallengeTestCallback sendSelectedAuthMethodCallback = new MFARequiredStateRequestChallengeTestCallback(); - AuthMethod authMethod = ((MFARequiredResult.SelectionRequired) getAuthMethodsResult).getAuthMethods().get(0); - nextState3.requestChallenge(authMethod, sendSelectedAuthMethodCallback); - - MFARequiredResult sendSelectedAuthMethodResult = sendSelectedAuthMethodCallback.get(); - assertTrue(sendSelectedAuthMethodResult instanceof MFARequiredResult.VerificationRequired); - - correlationId = UUID.randomUUID().toString(); - // 7a. Send challenge value to the API - // 7b. Sign in completed, receive tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendSelectedAuthMethodResult).getNextState()); - mockCorrelationId(nextState4, correlationId); - - SubmitChallengeTestCallback submitChallengeCallback = new SubmitChallengeTestCallback(); - nextState4.submitChallenge(code, submitChallengeCallback); - - MFASubmitChallengeResult submitChallengeResult = submitChallengeCallback.get(); - assertTrue(submitChallengeResult instanceof SignInResult.Complete); - } - - @Test - public void testSignInMFAScenario2() throws ExecutionException, InterruptedException, TimeoutException { - String correlationId = UUID.randomUUID().toString(); - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ); - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ); - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ); - SignInTestCallback signInCallback = new SignInTestCallback(); application.signIn( @@ -669,22 +555,6 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep correlationId = UUID.randomUUID().toString(); // 3a. Sign in challenge for default auth method // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.INTROSPECT_REQUIRED - ); - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - AwaitingMFAState nextState = spy(((SignInResult.MFARequired) result).getNextState()); - mockCorrelationId(nextState, correlationId); - - AwaitingMFAStateRequestChallengeTestCallback sendChallengeCallback = new AwaitingMFAStateRequestChallengeTestCallback(); - nextState.requestChallenge(sendChallengeCallback); - - MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); - assertTrue(sendChallengeResult instanceof MFARequiredResult.SelectionRequired); correlationId = UUID.randomUUID().toString(); // 6a. Sign in challenge for specified auth method @@ -697,15 +567,15 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState3 = spy(((MFARequiredResult.SelectionRequired) sendChallengeResult).getNextState()); - mockCorrelationId(nextState3, correlationId); + AwaitingMFAState nextState = spy(((SignInResult.MFARequired) result).getNextState()); + mockCorrelationId(nextState, correlationId); - MFARequiredStateRequestChallengeTestCallback sendSelectedAuthMethodCallback = new MFARequiredStateRequestChallengeTestCallback(); - AuthMethod authMethod = ((MFARequiredResult.SelectionRequired) sendChallengeResult).getAuthMethods().get(0); - nextState3.requestChallenge(authMethod, sendSelectedAuthMethodCallback); + AwaitingMFAStateRequestChallengeTestCallback sendChallengeCallback = new AwaitingMFAStateRequestChallengeTestCallback(); + AuthMethod authMethod = ((SignInResult.MFARequired) result).getAuthMethods().get(0); + nextState.requestChallenge(authMethod, sendChallengeCallback); - MFARequiredResult sendSelectedAuthMethodResult = sendSelectedAuthMethodCallback.get(); - assertTrue(sendSelectedAuthMethodResult instanceof MFARequiredResult.VerificationRequired); + MFARequiredResult sendChallengeResult = sendChallengeCallback.get(); + assertTrue(sendChallengeResult instanceof MFARequiredResult.VerificationRequired); correlationId = UUID.randomUUID().toString(); // 7a. Send challenge value to the API @@ -718,7 +588,7 @@ public void testSignInMFAScenario2() throws ExecutionException, InterruptedExcep // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendSelectedAuthMethodResult).getNextState()); + MFARequiredState nextState4 = spy(((MFARequiredResult.VerificationRequired) sendChallengeResult).getNextState()); mockCorrelationId(nextState4, correlationId); SubmitChallengeTestCallback submitChallengeCallback = new SubmitChallengeTestCallback(); @@ -3447,19 +3317,6 @@ public void onError(@NonNull BaseException exception) { } } -class GetAuthMethodsTestCallback extends TestCallback implements MFARequiredState.GetAuthMethodsCallback { - - @Override - public void onResult(MFAGetAuthMethodsResult result) { - future.setResult(result); - } - - @Override - public void onError(@NonNull BaseException exception) { - future.setException(exception); - } -} - class MFARequiredStateRequestChallengeTestCallback extends TestCallback implements MFARequiredState.RequestChallengeCallback { @Override diff --git a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt index d48ad0cdb6..a78281b15a 100644 --- a/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt +++ b/msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt @@ -51,15 +51,13 @@ import com.microsoft.identity.nativeauth.parameters.NativeAuthSignUpParameters import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenError import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenErrorTypes -import com.microsoft.identity.nativeauth.statemachine.errors.MFAGetAuthMethodsError -import com.microsoft.identity.nativeauth.statemachine.errors.MFARequestChallengeError +import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordSubmitPasswordError import com.microsoft.identity.nativeauth.statemachine.errors.SignInContinuationError import com.microsoft.identity.nativeauth.statemachine.errors.SignInError import com.microsoft.identity.nativeauth.statemachine.errors.SignUpError import com.microsoft.identity.nativeauth.statemachine.errors.SignUpSubmitAttributesError -import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.SubmitCodeError import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult @@ -2637,212 +2635,6 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) assertTrue((submitCodeResult as SubmitCodeError).error.equals("unsuccessful_command")) // ClientException will be caught in CommandResultUtil.kt and converted to generic error in interface layer } - @Test - fun testSignInMFAVerificationRequiredGetAuthMethodsComplete() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState = spy((result as SignInResult.MFARequired).nextState) - nextState.mockCorrelationId(correlationId) - - // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods - // 4b. Return list of auth methods - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.INTROSPECT_SUCCESS - ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) - - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertNotNull(getAuthMethodsResult.authMethods) - - correlationId = UUID.randomUUID().toString() - // 5a. Sign in challenge for specified auth method - // 5b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(getAuthMethodsResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 6a. Token with oob - // 6b. Success, with tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) - nextState4.mockCorrelationId(correlationId) - - val submitChallengeResult = nextState4.submitChallenge(code) - assertResult(submitChallengeResult) - } - - @Test - fun testSignInMFASelectionRequiredGetAuthMethodsComplete() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - correlationId = UUID.randomUUID().toString() - // 4a. Sign in challenge for default auth method - // 3b. Setup server response with introspect_required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.INTROSPECT_REQUIRED - ) - - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.INTROSPECT_SUCCESS - ) - // Initiate challenge, send code to email - val nextState = spy((result as SignInResult.MFARequired).nextState) - - nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.SelectionRequired) - assertNotNull(sendChallengeResult.authMethods) - - correlationId = UUID.randomUUID().toString() - // 5a. Sign in challenge for specified auth method - // 5b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(sendChallengeResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(sendChallengeResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 6a. Token with oob - // 6b. Success, with tokens - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.TOKEN_SUCCESS - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) - nextState4.mockCorrelationId(correlationId) - - val submitChallengeResult = nextState4.submitChallenge(code) - assertResult(submitChallengeResult) - } - @Test fun testSignInMFAVerificationRequiredComplete() = runTest { // 1. Sign in initiate with username @@ -2888,7 +2680,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -2914,7 +2706,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) } @Test - fun testSignInMFASelectionRequiredGetAuthMethodsRedirect() = runTest { + fun testSignInMFARequiredReturnsEmailAuthMethod() = runTest { // 1. Sign in initiate with username // 1a. Setup server response var correlationId = UUID.randomUUID().toString() @@ -2943,68 +2735,19 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val result = application.signIn(username, password) assertResult(result) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. correlationId = UUID.randomUUID().toString() - // 4a. Sign in challenge for default auth method - // 3b. Setup server response with introspect_required + // 3a. Sign in challenge for default auth method + // 3b. Setup server response with oob required configureMockApi( MockApiEndpoint.SignInChallenge, correlationId, - MockApiResponseType.INTROSPECT_REQUIRED + MockApiResponseType.CHALLENGE_TYPE_OOB ) - + // 3c. Setup oauth/introspect to return email auth method configureMockApi( MockApiEndpoint.Introspect, correlationId, - MockApiResponseType.CHALLENGE_TYPE_REDIRECT - ) - // Initiate challenge, send code to email - val nextState = spy((result as SignInResult.MFARequired).nextState) - - nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - assertTrue((sendChallengeResult as MFARequestChallengeError).isBrowserRequired()) - } - - @Test - fun testSignInMFAVerificationRequiredGetAuthMethodsRedirect() = runTest { - // 1. Sign in initiate with username - // 1a. Setup server response - var correlationId = UUID.randomUUID().toString() - configureMockApi( - MockApiEndpoint.SignInInitiate, - correlationId, - MockApiResponseType.INITIATE_SUCCESS - ) - - // 2a. Sign in challenge - // 2b. Setup server response with password required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_PASSWORD - ) - - // 3a. Token with password - // 3b. mfa_required - configureMockApi( - MockApiEndpoint.SignInToken, - correlationId, - MockApiResponseType.MFA_REQUIRED - ) - - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB + MockApiResponseType.INTROSPECT_SUCCESS ) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock @@ -3013,30 +2756,12 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) nextState.mockCorrelationId(correlationId) // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) assertNotNull(sendChallengeResult.codeLength) assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods - // 4b. Return list of auth methods - configureMockApi( - MockApiEndpoint.Introspect, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_REDIRECT - ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) - - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - assertTrue((getAuthMethodsResult as MFAGetAuthMethodsError).isBrowserRequired()) } @Test @@ -3084,7 +2809,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -3156,51 +2881,22 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.MFA_REQUIRED ) - val result = application.signIn(username, password) - assertResult(result) - - correlationId = UUID.randomUUID().toString() - // 3a. Sign in challenge for default auth method - // 3b. Setup server response with oob required - configureMockApi( - MockApiEndpoint.SignInChallenge, - correlationId, - MockApiResponseType.CHALLENGE_TYPE_OOB - ) - - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState = spy((result as SignInResult.MFARequired).nextState) - nextState.mockCorrelationId(correlationId) - - // Initiate challenge, send code to email - val sendChallengeResult = nextState.requestChallenge() - assertResult(sendChallengeResult) - (sendChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendChallengeResult.sentTo) - assertNotNull(sendChallengeResult.codeLength) - assertNotNull(sendChallengeResult.channel) - - correlationId = UUID.randomUUID().toString() - // 4a. Call /introspect to get additional methods + // 4a. Call /introspect to get methods // 4b. Return list of auth methods configureMockApi( MockApiEndpoint.Introspect, correlationId, MockApiResponseType.INTROSPECT_SUCCESS ) + + val result = application.signIn(username, password) + assertResult(result) + // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState2 = spy(sendChallengeResult.nextState) - nextState2.mockCorrelationId(correlationId) - - // Call /introspect to get all auth methods - val getAuthMethodsResult = nextState2.getAuthMethods() - assertResult(getAuthMethodsResult) - (getAuthMethodsResult as MFARequiredResult.SelectionRequired) - assertNotNull(getAuthMethodsResult.authMethods) + val nextState = spy((result as SignInResult.MFARequired).nextState) + nextState.mockCorrelationId(correlationId) - correlationId = UUID.randomUUID().toString() // 5a. Sign in challenge for specified auth method // 5b. Setup server response with oob required configureMockApi( @@ -3209,18 +2905,13 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) MockApiResponseType.CHALLENGE_TYPE_OOB ) - // correlation ID field in will be null, because the mock API doesn't return this. So, we mock - // it's value in order to make it consistent with the subsequent call to mock API. - val nextState3 = spy(getAuthMethodsResult.nextState) - nextState3.mockCorrelationId(correlationId) - - // Call /challenge with specified ID - val sendSpecifiedChallengeResult = nextState3.requestChallenge(getAuthMethodsResult.authMethods[0]) - assertResult(sendSpecifiedChallengeResult) - (sendSpecifiedChallengeResult as MFARequiredResult.VerificationRequired) - assertNotNull(sendSpecifiedChallengeResult.sentTo) - assertNotNull(sendSpecifiedChallengeResult.codeLength) - assertNotNull(sendSpecifiedChallengeResult.channel) + // Initiate challenge, send code to email + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) + assertResult(sendChallengeResult) + (sendChallengeResult as MFARequiredResult.VerificationRequired) + assertNotNull(sendChallengeResult.sentTo) + assertNotNull(sendChallengeResult.codeLength) + assertNotNull(sendChallengeResult.channel) correlationId = UUID.randomUUID().toString() // 6a. Token with oob @@ -3233,7 +2924,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState4 = spy(sendSpecifiedChallengeResult.nextState) + val nextState4 = spy(sendChallengeResult.nextState) nextState4.mockCorrelationId(correlationId) val submitChallengeResult = nextState4.submitChallenge(code) @@ -3251,7 +2942,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) // correlation ID field in will be null, because the mock API doesn't return this. So, we mock // it's value in order to make it consistent with the subsequent call to mock API. - val nextState5 = spy(sendSpecifiedChallengeResult.nextState) + val nextState5 = spy(sendChallengeResult.nextState) nextState4.mockCorrelationId(correlationId) // 8b. Call SDK interface @@ -3305,7 +2996,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) val nextState = spy((result as SignInResult.MFARequired).nextState) nextState.mockCorrelationId(correlationId) - val sendChallengeResult = nextState.requestChallenge() + val sendChallengeResult = nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) (sendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(sendChallengeResult.sentTo) @@ -3327,7 +3018,7 @@ class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) nextState2.mockCorrelationId(correlationId) // Resend - val resendChallengeResult = nextState2.requestChallenge() + val resendChallengeResult = nextState2.requestChallenge(result.authMethods.first()) assertResult(resendChallengeResult) (resendChallengeResult as MFARequiredResult.VerificationRequired) assertNotNull(resendChallengeResult.sentTo) From e858ccc62bcb087347e700694089d2aab2df7f58 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Thu, 18 Sep 2025 14:18:25 +0100 Subject: [PATCH 13/17] update common --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index 26deb12c26..fa49d9cdd5 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 26deb12c26bf91916a55be3c5f9fad6b8bfff9e6 +Subproject commit fa49d9cdd546dd9cd17fab347fb6605cb125514e From 2cd25ad1c3a0fcdf7c3164772b81bdb14ef5f686 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Mon, 22 Sep 2025 15:59:02 +0100 Subject: [PATCH 14/17] resolve conflict --- common | 2 +- .../identity/nativeauth/statemachine/results/SignInResult.kt | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/common b/common index 9dfc3333a3..de15c9aff5 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 9dfc3333a3807f86dd812bccdc462d08c68e763c +Subproject commit de15c9aff5b87e210a69f03527bb308c1694a8d9 diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt index 31ee682865..b5de4de4b8 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/SignInResult.kt @@ -95,11 +95,7 @@ interface SignInResult : Result { class StrongAuthMethodRegistrationRequired( override val nextState: RegisterStrongAuthState, val authMethods: List -<<<<<<< HEAD ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult, SignInSubmitCodeResult -======= - ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInSubmitPasswordResult ->>>>>>> feature/email-otp-mfa } /** From 1149023ea741253fd61b7ff61d3aeaca01c18436 Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 24 Sep 2025 12:30:00 +0100 Subject: [PATCH 15/17] update changelog and common --- changelog | 2 ++ common | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index d03c7b9598..57c09ac512 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,8 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr vNext ---------- - [MAJOR] Update proguard rules (#2372) +- [MINOR] SDK now handles SMS as strong authentication method (#2382) +- [MINOR] Awaiting MFA Delegate now automatically returns the AuthMethods to be used when calling MFA Challenge (#2380) Version 7.1.0 ---------- diff --git a/common b/common index ff7c7647d3..9f5aba7905 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit ff7c7647d3df13b45961f74bbde919ec616e7b71 +Subproject commit 9f5aba7905fd8ea5ee5a4c3902dae23ab2c806ba From 473efabf4e1dcfe661e9afa58ad30e4cf87b364f Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Wed, 24 Sep 2025 15:57:12 +0100 Subject: [PATCH 16/17] make verification contact mandatory --- common | 2 +- .../NativeAuthPublicClientApplication.kt | 1 - ...NativeAuthChallengeAuthMethodParameters.kt | 7 +- .../statemachine/states/JITStates.kt | 17 +--- .../statemachine/states/SignInStates.kt | 7 +- .../tests/network/nativeauth/SignInJITTest.kt | 81 ++----------------- 6 files changed, 17 insertions(+), 98 deletions(-) diff --git a/common b/common index 9f5aba7905..ad29eb02b7 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 9f5aba7905fd8ea5ee5a4c3902dae23ab2c806ba +Subproject commit ad29eb02b7286bfb1fbd4d62ef942ce9ec21f395 diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt index a829b3fc21..08d78c04c2 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt @@ -780,7 +780,6 @@ class NativeAuthPublicClientApplication( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, - username = username, config = nativeAuthConfig ), authMethods = result.authMethods.toListOfAuthMethods() diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt index 8efad96dc8..1da62d7470 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/parameters/NativeAuthChallengeAuthMethodParameters.kt @@ -32,11 +32,10 @@ class NativeAuthChallengeAuthMethodParameters( /** * authentication method to challenge */ - val authMethod: AuthMethod -) { + val authMethod: AuthMethod, /** * email or phone number to contact to register a new strong authentication method */ - var verificationContact: String? = null -} \ No newline at end of file + var verificationContact: String +) \ No newline at end of file diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt index 6fa6324c61..c759252be6 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.withContext abstract class BaseJITSubmitChallengeState( override val continuationToken: String, override val correlationId: String, - internal open val username: String, private val config: NativeAuthPublicClientApplicationConfiguration ) : BaseState(continuationToken = continuationToken, correlationId = correlationId), State, Parcelable { suspend fun internalChallengeAuthMethod(parameters: NativeAuthChallengeAuthMethodParameters, tag: String): RegisterStrongAuthChallengeResult { @@ -48,8 +47,7 @@ abstract class BaseJITSubmitChallengeState( tag, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications." ) - // when SMS auth method is used verification contact can't be nil or empty - if (isChallengeChannelSMS(parameters.authMethod.challengeChannel) && parameters.verificationContact.isNullOrBlank()) { + if (parameters.verificationContact.isBlank()) { return RegisterStrongAuthChallengeError( errorType = ErrorTypes.INVALID_INPUT, errorMessage = "Invalid verification contact", @@ -57,13 +55,11 @@ abstract class BaseJITSubmitChallengeState( ) } - // if external developer does not provide a verification contact, we use the username - val verificationContact: String = parameters.verificationContact.takeIf { !it.isNullOrBlank() } ?: username val params = CommandParametersAdapter.createJITChallengeAuthMethodCommandParameters( config, config.oAuth2TokenCache, - verificationContact, + parameters.verificationContact, parameters.authMethod.challengeChannel, parameters.authMethod.challengeType, correlationId, @@ -131,7 +127,6 @@ abstract class BaseJITSubmitChallengeState( nextState = RegisterStrongAuthVerificationRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, - username = username, config = config ), codeLength = result.codeLength, @@ -161,9 +156,8 @@ abstract class BaseJITSubmitChallengeState( class RegisterStrongAuthState( override val continuationToken: String, override val correlationId: String, - override val username: String, private val config: NativeAuthPublicClientApplicationConfiguration -) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config, username = username), State, Parcelable { +) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config), State, Parcelable { private val TAG: String = RegisterStrongAuthState::class.java.simpleName /** @@ -216,7 +210,6 @@ class RegisterStrongAuthState( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", - username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration ) @@ -245,9 +238,8 @@ class RegisterStrongAuthState( class RegisterStrongAuthVerificationRequiredState( override val continuationToken: String, override val correlationId: String, - override val username: String, private val config: NativeAuthPublicClientApplicationConfiguration -) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, username = username, config = config) { +) : BaseJITSubmitChallengeState(continuationToken = continuationToken, correlationId = correlationId, config = config) { private val TAG: String = RegisterStrongAuthVerificationRequiredState::class.java.simpleName @@ -427,7 +419,6 @@ class RegisterStrongAuthVerificationRequiredState( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", - username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration ) diff --git a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt index 18ef2252ca..debd25c9b8 100644 --- a/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/SignInStates.kt @@ -85,8 +85,8 @@ class SignInCodeRequiredState internal constructor( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", - username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", + username = parcel.readString() ?: "", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration @@ -219,7 +219,6 @@ class SignInCodeRequiredState internal constructor( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, - username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() @@ -391,8 +390,8 @@ class SignInPasswordRequiredState( private val TAG: String = SignInPasswordRequiredState::class.java.simpleName constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", - username = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", + username = parcel.readString() ?: "", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration @@ -488,7 +487,6 @@ class SignInPasswordRequiredState( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, - username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() @@ -743,7 +741,6 @@ class SignInContinuationState( nextState = RegisterStrongAuthState( continuationToken = result.continuationToken, correlationId = result.correlationId, - username = username, config = config ), authMethods = result.authMethods.toListOfAuthMethods() diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt index 5a4233e8bd..09d90c9fea 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt @@ -34,27 +34,16 @@ import com.microsoft.identity.nativeauth.parameters.NativeAuthGetAccessTokenPara import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInContinuationParameters import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInParameters import com.microsoft.identity.nativeauth.parameters.NativeAuthSignUpParameters -import com.microsoft.identity.nativeauth.statemachine.errors.MFASubmitChallengeError import com.microsoft.identity.nativeauth.statemachine.errors.SignInError import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult -import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult import com.microsoft.identity.nativeauth.statemachine.results.RegisterStrongAuthChallengeResult import com.microsoft.identity.nativeauth.statemachine.results.SignInResult -import com.microsoft.identity.nativeauth.statemachine.results.SignUpResendCodeResult import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult -import com.microsoft.identity.nativeauth.statemachine.states.RegisterStrongAuthVerificationRequiredState import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue -import org.junit.Assert.fail import org.junit.Ignore import org.junit.Test -import org.robolectric.RuntimeEnvironment.application -import org.robolectric.shadows.ShadowPackageManager.resources -import org.robolectric.versioning.AndroidVersions -import java.util.Base64 class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { @@ -108,10 +97,10 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { assertResult(signInResult) val authMethod = (signInResult as SignInResult.StrongAuthMethodRegistrationRequired).authMethods[0] - // Specify a different email as verification contact. - val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod) val contact = tempEmailApi.generateRandomEmailAddressLocally() - authMethodParams.verificationContact = contact + + // Specify a different email as verification contact. + val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod, contact) // Complete JIT. Verification email should be sent to the second email. val challengeResult = signInResult.nextState.challengeAuthMethod(authMethodParams) @@ -130,63 +119,6 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { } } - /** - * Full flow: Ensure JIT is triggered in signIn after signUp (preverified) - * - SignUp a new user with username and password. - * - SignIn after signUp with authentication context as claims to trigger MFA. // TODO: tenant setting - * - Check that JIT flow is triggered. - * - Do not specify a verification contact. - * - SignIn should be completed without needs to send a code to the email. - * - Access token is received. - * - */ - @Ignore("Retrieving OTP code failure.") - @Test - fun `test sign after sign up without specify verification contact`() { - config = getConfig(defaultConfigType) - application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) - resources = config.resources - val authenticationContextId = "c4" - val authenticationContextRequestClaimJson = "{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"$authenticationContextId\"}}}" - - retryOperation { - runBlocking { - // SignUp a new user with username and password. - val username = tempEmailApi.generateRandomEmailAddressLocally() - val signUpParams = NativeAuthSignUpParameters(username) - signUpParams.password = getSafePassword().toCharArray() - val signUpResult = application.signUp(signUpParams) - assertResult(signUpResult) - val otp1 = tempEmailApi.retrieveCodeFromInbox(username) - val submitCodeResult = (signUpResult as SignUpResult.CodeRequired).nextState.submitCode(otp1) - assertResult(submitCodeResult) - - // SignIn after signUp with authentication context as claims to trigger MFA. - val continuationParameters = NativeAuthSignInContinuationParameters() - continuationParameters.claimsRequest = ClaimsRequest.getClaimsRequestFromJsonString(authenticationContextRequestClaimJson) - val signWithContinuationResult = (submitCodeResult as SignUpResult.Complete).nextState.signIn(continuationParameters) - - // Check that JIT flow is triggered. - assertResult(signWithContinuationResult) - val authMethod = (signWithContinuationResult as SignInResult.StrongAuthMethodRegistrationRequired).authMethods[0] - // Do not specify a verification contact. - val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod) - - // SignIn should be completed without needs to send a code to the email. - val challengeResult = signWithContinuationResult.nextState.challengeAuthMethod(authMethodParams) - assertResult(challengeResult) - - // Access token is received. - val accountState = (challengeResult as SignInResult.Complete).resultValue - val accountParam = NativeAuthGetAccessTokenParameters() - val getAccessTokenResult = accountState.getAccessToken(accountParam) - assertResult(getAccessTokenResult) - val authResult = (getAccessTokenResult as GetAccessTokenResult.Complete).resultValue - assertNotNull(authResult) - } - } - } - /** * Full flow: Ensure JIT is triggered in signIn after signUp and a second email is used as verification contact * - SignUp a new user with username and password. @@ -226,10 +158,11 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { // Check that JIT flow is triggered. assertResult(signWithContinuationResult) val authMethod = (signWithContinuationResult as SignInResult.StrongAuthMethodRegistrationRequired).authMethods[0] - // Specify a different email as verification contact. - val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod) + val contact = tempEmailApi.generateRandomEmailAddressLocally() - authMethodParams.verificationContact = contact + + // Specify a different email as verification contact. + val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod, contact) // Complete JIT. Verification email should be sent to the second email. val challengeResult = signWithContinuationResult.nextState.challengeAuthMethod(authMethodParams) From a9a7ce484cbbedc8878664cbeb041497a90e0d8a Mon Sep 17 00:00:00 2001 From: Danilo Raspa Date: Thu, 25 Sep 2025 10:47:22 +0100 Subject: [PATCH 17/17] point to common dev --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index ad29eb02b7..b89a1d9961 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit ad29eb02b7286bfb1fbd4d62ef942ce9ec21f395 +Subproject commit b89a1d9961e5ac1176fcaad9a254cf074470d16c