Skip to content

Commit cbd5e61

Browse files
SammyOYuki-YuXin
andauthored
Native auth MFA (#2167)
Support for email OTP based MFA in native authentication flows. - Added MFA results and states to the SDK interface - Business logic & API integration - Custom error handling for MFA required error during calls to /token with grant_type=refresh_token - Private preview warnings in console and documentation - Unit and E2E production tests MSAL common PR: AzureAD/microsoft-authentication-library-common-for-android#2489 --------- Co-authored-by: Yuki-YuXin <[email protected]>
1 parent 5850d47 commit cbd5e61

26 files changed

+2655
-254
lines changed

changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ vNext
44
----------
55
-[MINOR] Moving OS version check for passkeys (#2138)
66
-[PATCH] Fix Native Auth authority data being persisted across different SDK instances (#2159)
7+
-[MINOR] Support for email OTP MFA in native authentication (#2167)
78

89
Version 5.5.0
910
---------

common

Submodule common updated 31 files

msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java

Lines changed: 203 additions & 11 deletions
Large diffs are not rendered by default.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.nativeauth
24+
25+
import android.os.Parcel
26+
import android.os.Parcelable
27+
import com.microsoft.identity.common.java.nativeauth.providers.responses.signin.AuthenticationMethodApiResult
28+
import com.microsoft.identity.common.java.nativeauth.util.ILoggable
29+
import com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState
30+
import com.microsoft.identity.nativeauth.utils.serializable
31+
32+
/**
33+
* AuthMethod represents a user's authentication methods.
34+
*/
35+
data class AuthMethod(
36+
// Auth method ID
37+
val id: String,
38+
39+
// Auth method challenge type (oob, etc.)
40+
val challengeType: String,
41+
42+
// Auth method login hint (e.g. [email protected])
43+
val loginHint: String,
44+
45+
// Auth method challenge channel (email, etc.)
46+
val challengeChannel: String,
47+
) : ILoggable, Parcelable {
48+
override fun toUnsanitizedString(): String = "AuthMethod(id=$id, " +
49+
"challengeType=$challengeType, loginHint=$loginHint, challengeChannel=$challengeChannel)"
50+
51+
override fun toString(): String = "AuthMethod(id=$id, challengeType=$challengeType, challengeChannel=$challengeChannel)"
52+
53+
constructor(parcel: Parcel) : this(
54+
id = parcel.readString() ?: "",
55+
challengeType = parcel.readString() ?: "",
56+
loginHint = parcel.readString() ?: "",
57+
challengeChannel = parcel.readString() ?: ""
58+
)
59+
60+
override fun describeContents(): Int {
61+
return 0
62+
}
63+
64+
override fun writeToParcel(parcel: Parcel, flags: Int) {
65+
parcel.writeString(id)
66+
parcel.writeString(challengeType)
67+
parcel.writeString(loginHint)
68+
parcel.writeString(challengeChannel)
69+
}
70+
71+
companion object CREATOR : Parcelable.Creator<AuthMethod> {
72+
override fun createFromParcel(parcel: Parcel): AuthMethod {
73+
return AuthMethod(parcel)
74+
}
75+
76+
override fun newArray(size: Int): Array<AuthMethod?> {
77+
return arrayOfNulls(size)
78+
}
79+
}
80+
}
81+
82+
/**
83+
* Converts a list of auth method API response to a list of [AuthMethod] objects
84+
*/
85+
internal fun List<AuthenticationMethodApiResult>.toListOfAuthMethods(): List<AuthMethod> {
86+
return this.map { it.toAuthMethod() }
87+
}
88+
89+
/**
90+
* Converts an [AuthenticationMethodApiResult] API response to an [AuthMethod] object
91+
*/
92+
internal fun AuthenticationMethodApiResult.toAuthMethod(): AuthMethod {
93+
return AuthMethod(
94+
id = this.id,
95+
challengeType = this.challengeType,
96+
loginHint = this.loginHint,
97+
challengeChannel = this.challengeChannel
98+
)
99+
}

msal/src/main/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplication.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordStart
7272
import com.microsoft.identity.nativeauth.statemachine.results.SignInResult
7373
import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult
7474
import com.microsoft.identity.nativeauth.statemachine.states.AccountState
75+
import com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState
7576
import com.microsoft.identity.nativeauth.statemachine.states.Callback
7677
import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordCodeRequiredState
7778
import com.microsoft.identity.nativeauth.statemachine.states.SignInCodeRequiredState
@@ -334,7 +335,6 @@ class NativeAuthPublicClientApplication(
334335
)
335336
return withContext(Dispatchers.IO) {
336337
try {
337-
338338
verifyNoUserIsSignedIn()
339339

340340
if (username.isBlank()) {
@@ -393,7 +393,6 @@ class NativeAuthPublicClientApplication(
393393
)
394394
}
395395
}
396-
397396
is SignInCommandResult.CodeRequired -> {
398397
Logger.warn(
399398
TAG,
@@ -412,7 +411,6 @@ class NativeAuthPublicClientApplication(
412411
channel = result.challengeChannel
413412
)
414413
}
415-
416414
is INativeAuthCommandResult.InvalidUsername -> {
417415
SignInError(
418416
errorType = ErrorTypes.INVALID_USERNAME,
@@ -422,7 +420,6 @@ class NativeAuthPublicClientApplication(
422420
errorCodes = result.errorCodes
423421
)
424422
}
425-
426423
is SignInCommandResult.PasswordRequired -> {
427424
if (hasPassword) {
428425
Logger.warnWithObject(
@@ -446,7 +443,6 @@ class NativeAuthPublicClientApplication(
446443
)
447444
}
448445
}
449-
450446
is SignInCommandResult.UserNotFound -> {
451447
SignInError(
452448
errorType = ErrorTypes.USER_NOT_FOUND,
@@ -456,7 +452,6 @@ class NativeAuthPublicClientApplication(
456452
errorCodes = result.errorCodes
457453
)
458454
}
459-
460455
is SignInCommandResult.InvalidCredentials -> {
461456
if (hasPassword) {
462457
SignInError(
@@ -480,7 +475,16 @@ class NativeAuthPublicClientApplication(
480475
)
481476
}
482477
}
483-
478+
is SignInCommandResult.MFARequired -> {
479+
SignInResult.MFARequired(
480+
nextState = AwaitingMFAState(
481+
continuationToken = result.continuationToken,
482+
correlationId = result.correlationId,
483+
scopes = scopes,
484+
config = nativeAuthConfig
485+
)
486+
)
487+
}
484488
is INativeAuthCommandResult.Redirect -> {
485489
SignInError(
486490
errorType = ErrorTypes.BROWSER_REQUIRED,
@@ -489,7 +493,6 @@ class NativeAuthPublicClientApplication(
489493
correlationId = result.correlationId
490494
)
491495
}
492-
493496
is INativeAuthCommandResult.APIError -> {
494497
SignInError(
495498
errorMessage = result.errorDescription,

msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/Error.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ internal class ErrorTypes {
5959
*/
6060
const val INVALID_CODE = "invalid_code"
6161

62+
/*
63+
* The INVALID_CODE value indicates the challenge provided by user is incorrect.
64+
* The code should be re-submitted.
65+
*/
66+
const val INVALID_CHALLENGE = "invalid_challenge"
67+
6268
/*
6369
* The USER_NOT_FOUND value indicates there was no account found with the provided email.
6470
* The flow should be restarted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.microsoft.identity.nativeauth.statemachine.errors
2+
3+
import com.microsoft.identity.nativeauth.statemachine.results.MFAGetAuthMethodsResult
4+
import com.microsoft.identity.nativeauth.statemachine.results.MFARequiredResult
5+
import com.microsoft.identity.nativeauth.statemachine.results.MFASubmitChallengeResult
6+
7+
/**
8+
* MFA request challenge error. Use the utility methods of this class
9+
* to identify and handle the error. This error is produced by
10+
* [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.requestChallenge] and
11+
* [com.microsoft.identity.nativeauth.statemachine.states.AwaitingMFAState.requestChallenge].
12+
* @param errorType the error type value of the error that occurred.
13+
* @param error the error returned by the authentication server.
14+
* @param errorMessage the error message returned by the authentication server.
15+
* @param correlationId a unique identifier for the request that can help in diagnostics.
16+
* @param errorCodes a list of specific error codes returned by the authentication server.
17+
* @param exception an internal unexpected exception that happened.
18+
*/
19+
class MFARequestChallengeError(
20+
override val errorType: String? = null,
21+
override val error: String? = null,
22+
override val errorMessage: String?,
23+
override val correlationId: String,
24+
override val errorCodes: List<Int>? = null,
25+
val subError: String? = null,
26+
override var exception: Exception? = null
27+
): MFARequiredResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception)
28+
29+
/**
30+
* MFA get authentication methods error. Use the utility methods of this class
31+
* to identify and handle the error. This error is produced by
32+
* [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods]
33+
* @param errorType the error type value of the error that occurred.
34+
* @param error the error returned by the authentication server.
35+
* @param errorMessage the error message returned by the authentication server.
36+
* @param correlationId a unique identifier for the request that can help in diagnostics.
37+
* @param errorCodes a list of specific error codes returned by the authentication server.
38+
* @param exception an internal unexpected exception that happened.
39+
*/
40+
class MFAGetAuthMethodsError(
41+
override val errorType: String? = null,
42+
override val error: String? = null,
43+
override val errorMessage: String?,
44+
override val correlationId: String,
45+
override val errorCodes: List<Int>? = null,
46+
val subError: String? = null,
47+
override var exception: Exception? = null
48+
): MFAGetAuthMethodsResult, BrowserRequiredError, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception)
49+
50+
/**
51+
* MFA submit challenge error. The user should use the utility methods of this class
52+
* to identify and handle the error. This error is produced by
53+
* [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.submitChallenge]
54+
* @param errorType the error type value of the error that occurred.
55+
* @param error the error returned by the authentication server.
56+
* @param errorMessage the error message returned by the authentication server.
57+
* @param correlationId a unique identifier for the request that can help in diagnostics.
58+
* @param errorCodes a list of specific error codes returned by the authentication server.
59+
* @param exception an internal unexpected exception that happened.
60+
*/
61+
class MFASubmitChallengeError(
62+
override val errorType: String? = null,
63+
override val error: String? = null,
64+
override val errorMessage: String?,
65+
override val correlationId: String,
66+
override val errorCodes: List<Int>? = null,
67+
val subError: String? = null,
68+
override var exception: Exception? = null
69+
): MFASubmitChallengeResult, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception)
70+
{
71+
fun isInvalidChallenge(): Boolean = this.errorType == ErrorTypes.INVALID_CHALLENGE
72+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
24+
package com.microsoft.identity.nativeauth.statemachine.results
25+
26+
import com.microsoft.identity.nativeauth.AuthMethod
27+
import com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState
28+
29+
/**
30+
* Results related to various MFA operations.
31+
*/
32+
interface MFARequiredResult: Result {
33+
34+
/**
35+
* Verification required result, which indicates that a challenge was sent to the user's auth method,
36+
* and the server expects the challenge to be verified.
37+
*
38+
* @param nextState [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState] the current state of the flow with follow-on methods.
39+
* @param codeLength the length of the challenge required by the server.
40+
* @param sentTo the email/phone number the challenge was sent to.
41+
* @param channel the channel(email/phone) the challenge was sent through.
42+
*/
43+
class VerificationRequired(
44+
override val nextState: MFARequiredState,
45+
val codeLength: Int,
46+
val sentTo: String,
47+
val channel: String,
48+
) : MFARequiredResult, Result.SuccessResult(nextState = nextState)
49+
50+
/**
51+
* Selection required result, which indicates that a specific authentication method must be selected, which
52+
* the server will send the challenge to (once sendChallenge() is called).
53+
*
54+
* @param nextState [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState] the current state of the flow with follow-on methods.
55+
* @param authMethods the authentication methods that can be used to complete the challenge flow.
56+
*/
57+
class SelectionRequired(
58+
override val nextState: MFARequiredState,
59+
val authMethods: List<AuthMethod>
60+
) : MFARequiredResult, MFAGetAuthMethodsResult, Result.SuccessResult(nextState = nextState)
61+
}
62+
63+
/**
64+
* Results related to get authentication methods operation, produced by
65+
* [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.getAuthMethods]
66+
*/
67+
interface MFAGetAuthMethodsResult : Result
68+
69+
/**
70+
* Results related to MFA submit challenge operation, produced by
71+
* [com.microsoft.identity.nativeauth.statemachine.states.MFARequiredState.submitChallenge]
72+
*/
73+
interface MFASubmitChallengeResult : Result

msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/results/ResetPasswordResult.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
package com.microsoft.identity.nativeauth.statemachine.results
2525

26-
import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes
2726
import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordCodeRequiredState
2827
import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordPasswordRequiredState
2928
import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState

0 commit comments

Comments
 (0)