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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ vNext
- [MINOR] Add query parameter for Android Release OS Version (#2754)
- [MINOR] Add client scenario to JwtRequestBody (#2755)
- [MINOR] Awaiting MFA Delegate now automatically returns the AuthMethods to be used when calling MFA Challenge (#2764)
- [MINOR] SDK now handles SMS as strong authentication method #2766

Version 22.1.0
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ class NativeAuthMsalController : BaseNativeAuthController() {
tokenApiResult = tokenApiResult
)
}
is SignInTokenApiResult.MFARequired -> {
// when MFA is required, we retrieve the list of auth methods available
performIntrospectCall(
oAuth2Strategy = oAuth2Strategy,
continuationToken = tokenApiResult.continuationToken,
correlationId = tokenApiResult.correlationId
).toSignInStartCommandResult() as SignInWithContinuationTokenCommandResult
}
is SignInTokenApiResult.JITRequired -> {
// when a registration of a new strong authentication method is required, we retrieve the list of auth methods available
performJITIntrospect(
Expand All @@ -244,8 +252,7 @@ class NativeAuthMsalController : BaseNativeAuthController() {
redirectReason = tokenApiResult.redirectReason
)
}
is SignInTokenApiResult.InvalidAuthenticationType,
is SignInTokenApiResult.MFARequired, is SignInTokenApiResult.CodeIncorrect,
is SignInTokenApiResult.InvalidAuthenticationType, is SignInTokenApiResult.CodeIncorrect,
is SignInTokenApiResult.UserNotFound, is SignInTokenApiResult.InvalidCredentials,
is SignInTokenApiResult.UnknownError -> {
Logger.warnWithObject(
Expand Down Expand Up @@ -325,9 +332,26 @@ class NativeAuthMsalController : BaseNativeAuthController() {
redirectReason = tokenApiResult.redirectReason
)
}
is SignInTokenApiResult.MFARequired -> {
// when MFA is required, we retrieve the list of auth methods available
performIntrospectCall(
oAuth2Strategy = oAuth2Strategy,
continuationToken = tokenApiResult.continuationToken,
correlationId = tokenApiResult.correlationId
).toSignInStartCommandResult() as SignInSubmitCodeCommandResult
}
is SignInTokenApiResult.JITRequired -> {
// when a registration of a new strong authentication method is required, we retrieve the list of auth methods available
performJITIntrospect(
oAuth2Strategy = oAuth2Strategy,
parameters = parametersWithScopes,
continuationToken = tokenApiResult.continuationToken,
correlationId = tokenApiResult.correlationId
).toSignInStartCommandResult() as SignInSubmitCodeCommandResult
}
is SignInTokenApiResult.UnknownError, is SignInTokenApiResult.InvalidAuthenticationType,
is SignInTokenApiResult.MFARequired, is SignInTokenApiResult.InvalidCredentials,
is SignInTokenApiResult.UserNotFound, is SignInTokenApiResult.JITRequired -> {
is SignInTokenApiResult.InvalidCredentials,
is SignInTokenApiResult.UserNotFound -> {
Logger.warnWithObject(
TAG,
tokenApiResult.correlationId,
Expand Down Expand Up @@ -641,6 +665,16 @@ class NativeAuthMsalController : BaseNativeAuthController() {
correlationId = result.correlationId
)
}
is JITChallengeApiResult.BlockedVerificationContact -> {
val customDescription = "Verification contact blocked. " +
"Please try using another email or phone number, or select an alternative authentication method."
JITCommandResult.BlockedVerificationContact(
error = result.error,
errorDescription = customDescription + result.errorDescription,
errorCodes = result.errorCodes,
correlationId = result.correlationId
)
}
is JITChallengeApiResult.OOBRequired -> {
JITCommandResult.VerificationRequired(
correlationId = result.correlationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ class NativeAuthControllerTest {
}

@Test
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkAuthMethods() {
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkEmailAuthMethod() {
val correlationId = UUID.randomUUID().toString()
MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.SignInToken,
Expand All @@ -591,6 +591,66 @@ class NativeAuthControllerTest {
assert(signInResult.authMethods.filter { it.challengeChannel == "email" }.count() == 1)
}

@Test
fun testSubmitCodeReturnMFARequiredIntrospectSuccess_checkEmailAuthMethod() {
val correlationId = UUID.randomUUID().toString()
MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.SignInToken,
correlationId = correlationId,
responseType = MockApiResponseType.MFA_REQUIRED
)

MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.Introspect,
correlationId = correlationId,
responseType = MockApiResponseType.INTROSPECT_SUCCESS
)

val signInCodeParameters = createSignInSubmitCodeCommandParameters(correlationId)
val signInResult = controller.signInSubmitCode(signInCodeParameters) as SignInCommandResult.MFARequired
assert(signInResult.authMethods.filter { it.challengeChannel == "email" }.count() == 1)
}

@Test
fun testSubmitCodeReturnMFARequiredIntrospectSuccess_checkSMSAuthMethod() {
val correlationId = UUID.randomUUID().toString()
MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.SignInToken,
correlationId = correlationId,
responseType = MockApiResponseType.MFA_REQUIRED
)

MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.Introspect,
correlationId = correlationId,
responseType = MockApiResponseType.INTROSPECT_SMS_SUCCESS
)

val signInCodeParameters = createSignInSubmitCodeCommandParameters(correlationId)
val signInResult = controller.signInSubmitCode(signInCodeParameters) as SignInCommandResult.MFARequired
assert(signInResult.authMethods.filter { it.challengeChannel == "sms" }.count() == 1)
}

@Test
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkSMSAuthMethod() {
val correlationId = UUID.randomUUID().toString()
MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.SignInToken,
correlationId = correlationId,
responseType = MockApiResponseType.MFA_REQUIRED
)

MockApiUtils.configureMockApi(
endpointType = MockApiEndpoint.Introspect,
correlationId = correlationId,
responseType = MockApiResponseType.INTROSPECT_SMS_SUCCESS
)

val signInParameters = createSignInSubmitPasswordCommandParameters(correlationId)
val signInResult = controller.signInSubmitPassword(signInParameters) as SignInCommandResult.MFARequired
assert(signInResult.authMethods.filter { it.challengeChannel == "sms" }.count() == 1)
}

@Test
fun `testMFAChallenge challenge returns redirect should return RedirectResult`() {
val correlationId = UUID.randomUUID().toString()
Expand Down Expand Up @@ -1606,7 +1666,7 @@ class NativeAuthControllerTest {
.build()
}

private fun createSignInSubmitCodeCommandParameters(correlationId: String, isMFAGrantYpe: Boolean = false): SignInSubmitCodeCommandParameters {
private fun createSignInSubmitCodeCommandParameters(correlationId: String, isMFAGrantType: Boolean = false): SignInSubmitCodeCommandParameters {
val authenticationScheme = AuthenticationSchemeFactory.createScheme(
AndroidPlatformComponentsFactory.createFromContext(context),
null
Expand All @@ -1622,7 +1682,7 @@ class NativeAuthControllerTest {
.oAuth2TokenCache(createCache())
.sdkType(SdkType.MSAL)
.correlationId(correlationId)
.isMFAGrantType(isMFAGrantYpe)
.isMFAGrantType(isMFAGrantType)
.requiredBrokerProtocolVersion(BrokerProtocolVersionUtil.MSAL_TO_BROKER_PROTOCOL_COMPRESSION_CHANGES_MINIMUM_VERSION)
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ interface JITCommandResult {
override fun toString(): String = "VerificationRequired(correlationId=$correlationId, codeLength=$codeLength, challengeChannel=$challengeChannel)"
}

data class BlockedVerificationContact(
override val correlationId: String,
val error: String,
val errorDescription: String,
val errorCodes: List<Int>
) : JITChallengeAuthMethodCommandResult {
override fun toUnsanitizedString(): String = "BlockedVerificationContact(correlationId=$correlationId, error=$error, errorDescription=$errorDescription, errorCodes=$errorCodes)"

override fun toString(): String = "BlockedVerificationContact(correlationId=$correlationId)"
}

data class IncorrectVerificationContact(
override val correlationId: String,
val error: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface SignInCommandResult {
override val correlationId: String,
val continuationToken: String,
val authMethods: List<AuthenticationMethodApiResult>
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInWithContinuationTokenCommandResult {
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInSubmitCodeCommandResult, SignInWithContinuationTokenCommandResult {
override fun toUnsanitizedString(): String = "StrongAuthMethodRegistrationRequired(correlationId=$correlationId, authMethods=${authMethods.toUnsanitizedString()})"

override fun toString(): String = "StrongAuthMethodRegistrationRequired(correlationId=$correlationId, authMethods=${authMethods})"
Expand Down Expand Up @@ -122,7 +122,7 @@ interface SignInCommandResult {
override val correlationId: String,
val continuationToken: String,
val authMethods: List<AuthenticationMethodApiResult>
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult {
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInSubmitCodeCommandResult, SignInWithContinuationTokenCommandResult {
override fun toUnsanitizedString(): String = "MFARequired(correlationId=$correlationId, authMethods=${authMethods.toUnsanitizedString()})"

override fun toString(): String = "MFARequired(correlationId=$correlationId, authMethods=${authMethods})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ object NativeAuthConstants {
object ChallengeChannel {
//Challenge is sent using the email channel
const val EMAIL = "email"
//Challenge is sent using the voice channel
const val VOICE = "voice"
//Challenge is sent using the SMS channel
const val SMS = "sms"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import com.microsoft.identity.common.java.nativeauth.providers.INativeAuthApiResponse
import com.microsoft.identity.common.java.nativeauth.providers.responses.ApiErrorResult
import com.microsoft.identity.common.java.nativeauth.util.isBlockedChallengeTarget
import com.microsoft.identity.common.java.nativeauth.util.isInvalidChallengeTarget
import com.microsoft.identity.common.java.nativeauth.util.isInvalidRequest
import com.microsoft.identity.common.java.nativeauth.util.isOOB
Expand Down Expand Up @@ -78,7 +79,14 @@ class JITChallengeApiResponse(
correlationId = correlationId
)
}

error.isInvalidRequest() && errorCodes?.first().isBlockedChallengeTarget() -> {
JITChallengeApiResult.BlockedVerificationContact(
error = error.orEmpty(),
errorDescription = errorDescription.orEmpty(),
errorCodes = errorCodes.orEmpty(),
correlationId = correlationId
)
}
else -> {
JITChallengeApiResult.UnknownError(
error = error.orEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ sealed interface JITChallengeApiResult: ApiResult {
}
}

data class BlockedVerificationContact(
override val correlationId: String,
override val error: String,
override val errorDescription: String,
override val errorCodes: List<Int>
) : ApiErrorResult(
error = error,
errorDescription = errorDescription,
errorCodes = errorCodes,
correlationId = correlationId
), JITChallengeApiResult {
override fun toUnsanitizedString() = "BlockedVerificationContact(correlationId=$correlationId, " +
"error=$error, errorDescription=$errorDescription, subError=$subError)"

override fun toString(): String = "BlockedVerificationContact(correlationId=$correlationId)"
}

data class InvalidVerificationContact(
override val correlationId: String,
override val error: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal fun AuthenticationMethodApiResponse.toAuthenticationMethodApiResult():
return AuthenticationMethodApiResult(
id = this.id ?: throw IllegalStateException("Required field id is empty"),
challengeType = this.challengeType ?: throw IllegalStateException("Required field challengeType is empty"),
loginHint = this.loginHint ?: throw IllegalStateException("Required loginHint id is empty"),
loginHint = this.loginHint,
challengeChannel = this.challengeChannel ?: throw IllegalStateException("Required challengeChannel id is empty")
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.microsoft.identity.common.java.nativeauth.util.ILoggable
data class AuthenticationMethodApiResult(
@Expose @SerializedName("id") val id: String,
@Expose @SerializedName("challenge_type") val challengeType: String,
@SerializedName("login_hint") val loginHint: String,
@SerializedName("login_hint") val loginHint: String?,
@Expose @SerializedName("challenge_channel") val challengeChannel: String,
) : ILoggable {
override fun toUnsanitizedString() = "AuthenticationMethod(id=$id, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ internal fun Int?.isInvalidChallengeTarget(): Boolean {
return this == 901001
}

internal fun Int?.isBlockedChallengeTarget(): Boolean {
return this == 550024
}

fun String?.isMFARequired(): Boolean {
return this.contentEquals(other = "mfa_required", ignoreCase = true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum class MockApiResponseType(val stringValue: String) {
EXPIRED_TOKEN("ExpiredToken"),
INITIATE_SUCCESS("InitiateSuccess"),
INTROSPECT_SUCCESS("IntrospectSuccess"),
INTROSPECT_SMS_SUCCESS("IntrospectSMSSuccess"),
INVALID_AUTHENTICATION_METHOD("InvalidAuthMethodForUser"),
INVALID_CLIENT("InvalidClient"),
INVALID_USERNAME("InvalidUsername"),
Expand Down