diff --git a/changelog b/changelog index d03c7b959..57c09ac51 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 a346e00d4..b89a1d996 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit a346e00d43a2ab5572bbb5a11e362686c905199b +Subproject commit b89a1d9961e5ac1176fcaad9a254cf074470d16c 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 cac95912a..2c8f31c40 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. @@ -40,9 +38,9 @@ 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, 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/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt index 497a72039..08d78c04c 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 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 9c1418d2b..1da62d747 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 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 + var verificationContact: String +) \ No newline at end of file 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 c93dc23d1..91c5456af 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 a3c31348f..e0c5e59ee 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/results/MFAResult.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/MFAResult.kt index f43a36c38..301be512e 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, 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 2e1e79f9a..b5de4de4b 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/JITStates.kt b/msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/JITStates.kt index 8434b1508..c759252be 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,20 @@ abstract class BaseJITSubmitChallengeState( tag, "Warning: this API is experimental. It may be changed in the future without notice. Do not use in production applications." ) - // 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 + if (parameters.verificationContact.isBlank()) { + return RegisterStrongAuthChallengeError( + errorType = ErrorTypes.INVALID_INPUT, + errorMessage = "Invalid verification contact", + correlationId = correlationId + ) + } + val params = CommandParametersAdapter.createJITChallengeAuthMethodCommandParameters( config, config.oAuth2TokenCache, - verificationContact, - challengeChannel, + parameters.verificationContact, + parameters.authMethod.challengeChannel, parameters.authMethod.challengeType, correlationId, continuationToken, @@ -108,6 +112,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( @@ -134,6 +147,10 @@ abstract class BaseJITSubmitChallengeState( } } } + + private fun isChallengeChannelSMS(challengeChannel: String): Boolean { + return challengeChannel.equals(NativeAuthConstants.ChallengeChannel.SMS, ignoreCase = true) + } } class RegisterStrongAuthState( 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 ca267ce63..debd25c9b 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 @@ -85,6 +86,7 @@ class SignInCodeRequiredState internal constructor( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", + username = parcel.readString() ?: "", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration @@ -201,6 +203,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( @@ -277,6 +300,7 @@ class SignInCodeRequiredState internal constructor( nextState = SignInCodeRequiredState( continuationToken = result.continuationToken, correlationId = result.correlationId, + username = username, scopes = scopes, config = config, claimsRequestJson = claimsRequestJson @@ -358,6 +382,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 @@ -366,6 +391,7 @@ class SignInPasswordRequiredState( constructor(parcel: Parcel) : this( continuationToken = parcel.readString() ?: "", correlationId = parcel.readString() ?: "UNSET", + username = parcel.readString() ?: "", scopes = parcel.createStringArrayList(), claimsRequestJson = parcel.readString(), config = parcel.serializable() as NativeAuthPublicClientApplicationConfiguration @@ -720,6 +746,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, 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 5a4233e8b..09d90c9fe 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)