Skip to content

Commit b89a1d9

Browse files
Native auth: SMS OTP MFA/JIT implementation, Fixes AB#3313635 (#2766)
This PR needs to be merged after the email OTP MFA one. This PR contains the changes to support SMS OTP MFA/JIT. Verification contact is now a mandatory field. MSAL PR: AzureAD/microsoft-authentication-library-for-android#2382 Fixes [AB#3313635](https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3313635) --------- Co-authored-by: Mustafa Mizrak <mustafamizrak@gmail.com>
1 parent a7bac98 commit b89a1d9

File tree

12 files changed

+148
-14
lines changed

12 files changed

+148
-14
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ vNext
77
- [MINOR] Add query parameter for Android Release OS Version (#2754)
88
- [MINOR] Add client scenario to JwtRequestBody (#2755)
99
- [MINOR] Awaiting MFA Delegate now automatically returns the AuthMethods to be used when calling MFA Challenge (#2764)
10+
- [MINOR] SDK now handles SMS as strong authentication method #2766
1011

1112
Version 22.1.0
1213
----------

common/src/main/java/com/microsoft/identity/common/nativeauth/internal/controllers/NativeAuthMsalController.kt

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ class NativeAuthMsalController : BaseNativeAuthController() {
229229
tokenApiResult = tokenApiResult
230230
)
231231
}
232+
is SignInTokenApiResult.MFARequired -> {
233+
// when MFA is required, we retrieve the list of auth methods available
234+
performIntrospectCall(
235+
oAuth2Strategy = oAuth2Strategy,
236+
continuationToken = tokenApiResult.continuationToken,
237+
correlationId = tokenApiResult.correlationId
238+
).toSignInStartCommandResult() as SignInWithContinuationTokenCommandResult
239+
}
232240
is SignInTokenApiResult.JITRequired -> {
233241
// when a registration of a new strong authentication method is required, we retrieve the list of auth methods available
234242
performJITIntrospect(
@@ -244,8 +252,7 @@ class NativeAuthMsalController : BaseNativeAuthController() {
244252
redirectReason = tokenApiResult.redirectReason
245253
)
246254
}
247-
is SignInTokenApiResult.InvalidAuthenticationType,
248-
is SignInTokenApiResult.MFARequired, is SignInTokenApiResult.CodeIncorrect,
255+
is SignInTokenApiResult.InvalidAuthenticationType, is SignInTokenApiResult.CodeIncorrect,
249256
is SignInTokenApiResult.UserNotFound, is SignInTokenApiResult.InvalidCredentials,
250257
is SignInTokenApiResult.UnknownError -> {
251258
Logger.warnWithObject(
@@ -325,9 +332,26 @@ class NativeAuthMsalController : BaseNativeAuthController() {
325332
redirectReason = tokenApiResult.redirectReason
326333
)
327334
}
335+
is SignInTokenApiResult.MFARequired -> {
336+
// when MFA is required, we retrieve the list of auth methods available
337+
performIntrospectCall(
338+
oAuth2Strategy = oAuth2Strategy,
339+
continuationToken = tokenApiResult.continuationToken,
340+
correlationId = tokenApiResult.correlationId
341+
).toSignInStartCommandResult() as SignInSubmitCodeCommandResult
342+
}
343+
is SignInTokenApiResult.JITRequired -> {
344+
// when a registration of a new strong authentication method is required, we retrieve the list of auth methods available
345+
performJITIntrospect(
346+
oAuth2Strategy = oAuth2Strategy,
347+
parameters = parametersWithScopes,
348+
continuationToken = tokenApiResult.continuationToken,
349+
correlationId = tokenApiResult.correlationId
350+
).toSignInStartCommandResult() as SignInSubmitCodeCommandResult
351+
}
328352
is SignInTokenApiResult.UnknownError, is SignInTokenApiResult.InvalidAuthenticationType,
329-
is SignInTokenApiResult.MFARequired, is SignInTokenApiResult.InvalidCredentials,
330-
is SignInTokenApiResult.UserNotFound, is SignInTokenApiResult.JITRequired -> {
353+
is SignInTokenApiResult.InvalidCredentials,
354+
is SignInTokenApiResult.UserNotFound -> {
331355
Logger.warnWithObject(
332356
TAG,
333357
tokenApiResult.correlationId,
@@ -641,6 +665,16 @@ class NativeAuthMsalController : BaseNativeAuthController() {
641665
correlationId = result.correlationId
642666
)
643667
}
668+
is JITChallengeApiResult.BlockedVerificationContact -> {
669+
val customDescription = "Verification contact blocked. " +
670+
"Please try using another email or phone number, or select an alternative authentication method."
671+
JITCommandResult.BlockedVerificationContact(
672+
error = result.error,
673+
errorDescription = customDescription + result.errorDescription,
674+
errorCodes = result.errorCodes,
675+
correlationId = result.correlationId
676+
)
677+
}
644678
is JITChallengeApiResult.OOBRequired -> {
645679
JITCommandResult.VerificationRequired(
646680
correlationId = result.correlationId,

common/src/test/java/com/microsoft/identity/common/nativeauth/internal/controllers/NativeAuthControllerTest.kt

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ class NativeAuthControllerTest {
572572
}
573573

574574
@Test
575-
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkAuthMethods() {
575+
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkEmailAuthMethod() {
576576
val correlationId = UUID.randomUUID().toString()
577577
MockApiUtils.configureMockApi(
578578
endpointType = MockApiEndpoint.SignInToken,
@@ -591,6 +591,66 @@ class NativeAuthControllerTest {
591591
assert(signInResult.authMethods.filter { it.challengeChannel == "email" }.count() == 1)
592592
}
593593

594+
@Test
595+
fun testSubmitCodeReturnMFARequiredIntrospectSuccess_checkEmailAuthMethod() {
596+
val correlationId = UUID.randomUUID().toString()
597+
MockApiUtils.configureMockApi(
598+
endpointType = MockApiEndpoint.SignInToken,
599+
correlationId = correlationId,
600+
responseType = MockApiResponseType.MFA_REQUIRED
601+
)
602+
603+
MockApiUtils.configureMockApi(
604+
endpointType = MockApiEndpoint.Introspect,
605+
correlationId = correlationId,
606+
responseType = MockApiResponseType.INTROSPECT_SUCCESS
607+
)
608+
609+
val signInCodeParameters = createSignInSubmitCodeCommandParameters(correlationId)
610+
val signInResult = controller.signInSubmitCode(signInCodeParameters) as SignInCommandResult.MFARequired
611+
assert(signInResult.authMethods.filter { it.challengeChannel == "email" }.count() == 1)
612+
}
613+
614+
@Test
615+
fun testSubmitCodeReturnMFARequiredIntrospectSuccess_checkSMSAuthMethod() {
616+
val correlationId = UUID.randomUUID().toString()
617+
MockApiUtils.configureMockApi(
618+
endpointType = MockApiEndpoint.SignInToken,
619+
correlationId = correlationId,
620+
responseType = MockApiResponseType.MFA_REQUIRED
621+
)
622+
623+
MockApiUtils.configureMockApi(
624+
endpointType = MockApiEndpoint.Introspect,
625+
correlationId = correlationId,
626+
responseType = MockApiResponseType.INTROSPECT_SMS_SUCCESS
627+
)
628+
629+
val signInCodeParameters = createSignInSubmitCodeCommandParameters(correlationId)
630+
val signInResult = controller.signInSubmitCode(signInCodeParameters) as SignInCommandResult.MFARequired
631+
assert(signInResult.authMethods.filter { it.challengeChannel == "sms" }.count() == 1)
632+
}
633+
634+
@Test
635+
fun testSubmitPasswordReturnMFARequiredIntrospectSuccess_checkSMSAuthMethod() {
636+
val correlationId = UUID.randomUUID().toString()
637+
MockApiUtils.configureMockApi(
638+
endpointType = MockApiEndpoint.SignInToken,
639+
correlationId = correlationId,
640+
responseType = MockApiResponseType.MFA_REQUIRED
641+
)
642+
643+
MockApiUtils.configureMockApi(
644+
endpointType = MockApiEndpoint.Introspect,
645+
correlationId = correlationId,
646+
responseType = MockApiResponseType.INTROSPECT_SMS_SUCCESS
647+
)
648+
649+
val signInParameters = createSignInSubmitPasswordCommandParameters(correlationId)
650+
val signInResult = controller.signInSubmitPassword(signInParameters) as SignInCommandResult.MFARequired
651+
assert(signInResult.authMethods.filter { it.challengeChannel == "sms" }.count() == 1)
652+
}
653+
594654
@Test
595655
fun `testMFAChallenge challenge returns redirect should return RedirectResult`() {
596656
val correlationId = UUID.randomUUID().toString()
@@ -1606,7 +1666,7 @@ class NativeAuthControllerTest {
16061666
.build()
16071667
}
16081668

1609-
private fun createSignInSubmitCodeCommandParameters(correlationId: String, isMFAGrantYpe: Boolean = false): SignInSubmitCodeCommandParameters {
1669+
private fun createSignInSubmitCodeCommandParameters(correlationId: String, isMFAGrantType: Boolean = false): SignInSubmitCodeCommandParameters {
16101670
val authenticationScheme = AuthenticationSchemeFactory.createScheme(
16111671
AndroidPlatformComponentsFactory.createFromContext(context),
16121672
null
@@ -1622,7 +1682,7 @@ class NativeAuthControllerTest {
16221682
.oAuth2TokenCache(createCache())
16231683
.sdkType(SdkType.MSAL)
16241684
.correlationId(correlationId)
1625-
.isMFAGrantType(isMFAGrantYpe)
1685+
.isMFAGrantType(isMFAGrantType)
16261686
.requiredBrokerProtocolVersion(BrokerProtocolVersionUtil.MSAL_TO_BROKER_PROTOCOL_COMPRESSION_CHANGES_MINIMUM_VERSION)
16271687
.build()
16281688
}

common4j/src/main/com/microsoft/identity/common/java/nativeauth/controllers/results/JITCommandResult.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ interface JITCommandResult {
3838
override fun toString(): String = "VerificationRequired(correlationId=$correlationId, codeLength=$codeLength, challengeChannel=$challengeChannel)"
3939
}
4040

41+
data class BlockedVerificationContact(
42+
override val correlationId: String,
43+
val error: String,
44+
val errorDescription: String,
45+
val errorCodes: List<Int>
46+
) : JITChallengeAuthMethodCommandResult {
47+
override fun toUnsanitizedString(): String = "BlockedVerificationContact(correlationId=$correlationId, error=$error, errorDescription=$errorDescription, errorCodes=$errorCodes)"
48+
49+
override fun toString(): String = "BlockedVerificationContact(correlationId=$correlationId)"
50+
}
51+
4152
data class IncorrectVerificationContact(
4253
override val correlationId: String,
4354
val error: String,

common4j/src/main/com/microsoft/identity/common/java/nativeauth/controllers/results/SignInCommandResult.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ interface SignInCommandResult {
7878
override val correlationId: String,
7979
val continuationToken: String,
8080
val authMethods: List<AuthenticationMethodApiResult>
81-
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInWithContinuationTokenCommandResult {
81+
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInSubmitCodeCommandResult, SignInWithContinuationTokenCommandResult {
8282
override fun toUnsanitizedString(): String = "StrongAuthMethodRegistrationRequired(correlationId=$correlationId, authMethods=${authMethods.toUnsanitizedString()})"
8383

8484
override fun toString(): String = "StrongAuthMethodRegistrationRequired(correlationId=$correlationId, authMethods=${authMethods})"
@@ -122,7 +122,7 @@ interface SignInCommandResult {
122122
override val correlationId: String,
123123
val continuationToken: String,
124124
val authMethods: List<AuthenticationMethodApiResult>
125-
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult {
125+
) : SignInStartCommandResult, SignInSubmitPasswordCommandResult, SignInSubmitCodeCommandResult, SignInWithContinuationTokenCommandResult {
126126
override fun toUnsanitizedString(): String = "MFARequired(correlationId=$correlationId, authMethods=${authMethods.toUnsanitizedString()})"
127127

128128
override fun toString(): String = "MFARequired(correlationId=$correlationId, authMethods=${authMethods})"

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthConstants.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ object NativeAuthConstants {
3434
object ChallengeChannel {
3535
//Challenge is sent using the email channel
3636
const val EMAIL = "email"
37-
//Challenge is sent using the voice channel
38-
const val VOICE = "voice"
3937
//Challenge is sent using the SMS channel
4038
const val SMS = "sms"
4139
}

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/jit/JITChallengeApiResponse.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.google.gson.annotations.Expose
2626
import com.google.gson.annotations.SerializedName
2727
import com.microsoft.identity.common.java.nativeauth.providers.INativeAuthApiResponse
2828
import com.microsoft.identity.common.java.nativeauth.providers.responses.ApiErrorResult
29+
import com.microsoft.identity.common.java.nativeauth.util.isBlockedChallengeTarget
2930
import com.microsoft.identity.common.java.nativeauth.util.isInvalidChallengeTarget
3031
import com.microsoft.identity.common.java.nativeauth.util.isInvalidRequest
3132
import com.microsoft.identity.common.java.nativeauth.util.isOOB
@@ -78,7 +79,14 @@ class JITChallengeApiResponse(
7879
correlationId = correlationId
7980
)
8081
}
81-
82+
error.isInvalidRequest() && errorCodes?.first().isBlockedChallengeTarget() -> {
83+
JITChallengeApiResult.BlockedVerificationContact(
84+
error = error.orEmpty(),
85+
errorDescription = errorDescription.orEmpty(),
86+
errorCodes = errorCodes.orEmpty(),
87+
correlationId = correlationId
88+
)
89+
}
8290
else -> {
8391
JITChallengeApiResult.UnknownError(
8492
error = error.orEmpty(),

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/jit/JITChallengeApiResult.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ sealed interface JITChallengeApiResult: ApiResult {
8080
}
8181
}
8282

83+
data class BlockedVerificationContact(
84+
override val correlationId: String,
85+
override val error: String,
86+
override val errorDescription: String,
87+
override val errorCodes: List<Int>
88+
) : ApiErrorResult(
89+
error = error,
90+
errorDescription = errorDescription,
91+
errorCodes = errorCodes,
92+
correlationId = correlationId
93+
), JITChallengeApiResult {
94+
override fun toUnsanitizedString() = "BlockedVerificationContact(correlationId=$correlationId, " +
95+
"error=$error, errorDescription=$errorDescription, subError=$subError)"
96+
97+
override fun toString(): String = "BlockedVerificationContact(correlationId=$correlationId)"
98+
}
99+
83100
data class InvalidVerificationContact(
84101
override val correlationId: String,
85102
override val error: String,

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/signin/AuthenticationMethodApiResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal fun AuthenticationMethodApiResponse.toAuthenticationMethodApiResult():
3333
return AuthenticationMethodApiResult(
3434
id = this.id ?: throw IllegalStateException("Required field id is empty"),
3535
challengeType = this.challengeType ?: throw IllegalStateException("Required field challengeType is empty"),
36-
loginHint = this.loginHint ?: throw IllegalStateException("Required loginHint id is empty"),
36+
loginHint = this.loginHint,
3737
challengeChannel = this.challengeChannel ?: throw IllegalStateException("Required challengeChannel id is empty")
3838
)
3939
}

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/signin/AuthenticationMethodApiResult.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.microsoft.identity.common.java.nativeauth.util.ILoggable
77
data class AuthenticationMethodApiResult(
88
@Expose @SerializedName("id") val id: String,
99
@Expose @SerializedName("challenge_type") val challengeType: String,
10-
@SerializedName("login_hint") val loginHint: String,
10+
@SerializedName("login_hint") val loginHint: String?,
1111
@Expose @SerializedName("challenge_channel") val challengeChannel: String,
1212
) : ILoggable {
1313
override fun toUnsanitizedString() = "AuthenticationMethod(id=$id, " +

0 commit comments

Comments
 (0)