Skip to content

fix(auth): Fix confirm signin when incorrect MFA code is entered #2286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 16, 2023
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
Expand Down Expand Up @@ -588,11 +588,19 @@ internal class RealAWSCognitoAuthPlugin(
authStateMachine.getCurrentState { authState ->
val authNState = authState.authNState
val signInState = (authNState as? AuthenticationState.SigningIn)?.signInState
when ((signInState as? SignInState.ResolvingChallenge)?.challengeState) {
is SignInChallengeState.WaitingForAnswer -> {
_confirmSignIn(challengeResponse, options, onSuccess, onError)
if (signInState is SignInState.ResolvingChallenge) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like code paths are still the same. This change looks unnecessary.

when (signInState.challengeState) {
is SignInChallengeState.WaitingForAnswer, is SignInChallengeState.Error -> {
_confirmSignIn(challengeResponse, options, onSuccess, onError)
}
else -> {
onError.accept(InvalidStateException())
}
}
else -> onError.accept(InvalidStateException())
} else if (authNState is AuthenticationState.SignedIn) {
onError.accept(SignedInException())
} else {
onError.accept(InvalidStateException())
}
}
}
Expand All @@ -610,7 +618,6 @@ internal class RealAWSCognitoAuthPlugin(
val authNState = authState.authNState
val authZState = authState.authZState
val signInState = (authNState as? AuthenticationState.SigningIn)?.signInState
val challengeState = (signInState as? SignInState.ResolvingChallenge)?.challengeState
when {
authNState is AuthenticationState.SignedIn &&
authZState is AuthorizationState.SessionEstablished -> {
Expand All @@ -625,21 +632,35 @@ internal class RealAWSCognitoAuthPlugin(
signInState is SignInState.Error -> {
authStateMachine.cancel(token)
onError.accept(
CognitoAuthExceptionConverter.lookup(signInState.exception, "Confirm Sign in failed.")
CognitoAuthExceptionConverter.lookup(
signInState.exception, "Confirm Sign in failed."
)
)
}
signInState is SignInState.ResolvingChallenge &&
signInState.challengeState is SignInChallengeState.Error -> {
(signInState.challengeState as SignInChallengeState.Error).exception?.let {
authStateMachine.cancel(token)
onError.accept(
CognitoAuthExceptionConverter.lookup(
error = it,
fallbackMessage = "Confirm Sign in failed."
)
)
(signInState.challengeState as? SignInChallengeState.Error)?.exception = null
}
}
}
},
{
val awsCognitoConfirmSignInOptions = options as? AWSCognitoAuthConfirmSignInOptions
val event = SignInChallengeEvent(
SignInChallengeEvent.EventType.VerifyChallengeAnswer(
challengeResponse,
awsCognitoConfirmSignInOptions?.metadata ?: mapOf()
)
}, {
val awsCognitoConfirmSignInOptions = options as? AWSCognitoAuthConfirmSignInOptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation looks off, I wonder how it passed lint check.

val event = SignInChallengeEvent(
SignInChallengeEvent.EventType.VerifyChallengeAnswer(
challengeResponse,
awsCognitoConfirmSignInOptions?.metadata ?: mapOf()
)
authStateMachine.send(event)
}
)
authStateMachine.send(event)
}
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
Expand All @@ -26,7 +26,6 @@ import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions
import com.amplifyframework.statemachine.codegen.data.AuthChallenge
import com.amplifyframework.statemachine.codegen.events.CustomSignInEvent
import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent
import com.amplifyframework.statemachine.codegen.events.SignInEvent

internal object SignInChallengeCognitoActions : SignInChallengeActions {
private const val KEY_SECRET_HASH = "SECRET_HASH"
Expand Down Expand Up @@ -81,20 +80,17 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions {
)
)
} catch (e: Exception) {
SignInEvent(SignInEvent.EventType.ThrowError(e))
SignInChallengeEvent(SignInChallengeEvent.EventType.ThrowError(e, challenge))
}
logger.verbose("$id Sending event ${evt.type}")
dispatcher.send(evt)
}

private fun getChallengeResponseKey(challengeName: String): String? {
val VALUE_ANSWER = "ANSWER"
val VALUE_SMS_MFA = "SMS_MFA_CODE"
val VALUE_NEW_PASSWORD = "NEW_PASSWORD"
return when (ChallengeNameType.fromValue(challengeName)) {
is ChallengeNameType.SmsMfa -> VALUE_SMS_MFA
is ChallengeNameType.NewPasswordRequired -> VALUE_NEW_PASSWORD
is ChallengeNameType.CustomChallenge -> VALUE_ANSWER
is ChallengeNameType.SmsMfa -> "SMS_MFA_CODE"
is ChallengeNameType.NewPasswordRequired -> "NEW_PASSWORD"
is ChallengeNameType.CustomChallenge -> "ANSWER"
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal class SignInChallengeEvent(val eventType: EventType, override val time:
data class VerifyChallengeAnswer(val answer: String, val metadata: Map<String, String>) : EventType()
data class FinalizeSignIn(val accessToken: String) : EventType()
data class Verified(val id: String = "") : EventType()
data class ThrowError(val exception: Exception, val challenge: AuthChallenge) : EventType()
}

override val type: String = eventType.javaClass.simpleName
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,7 @@ internal sealed class SignInChallengeState : State {
data class WaitingForAnswer(val challenge: AuthChallenge) : SignInChallengeState()
data class Verifying(val id: String = "") : SignInChallengeState()
data class Verified(val id: String = "") : SignInChallengeState()
data class Error(var exception: Exception?, val challenge: AuthChallenge) : SignInChallengeState()

class Resolver(private val challengeActions: SignInChallengeActions) : StateMachineResolver<SignInChallengeState> {
override val defaultState: SignInChallengeState = NotStarted()
Expand Down Expand Up @@ -58,8 +59,24 @@ internal sealed class SignInChallengeState : State {
}
is Verifying -> when (challengeEvent) {
is SignInChallengeEvent.EventType.Verified -> StateResolution(Verified())
is SignInChallengeEvent.EventType.ThrowError -> {
StateResolution(Error(challengeEvent.exception, challengeEvent.challenge), listOf())
}

else -> defaultResolution
}
is Error -> {
when (challengeEvent) {
is SignInChallengeEvent.EventType.VerifyChallengeAnswer -> {
val action = challengeActions.verifyChallengeAuthAction(challengeEvent, oldState.challenge)
StateResolution(Verifying(oldState.challenge.challengeName), listOf(action))
}
is SignInChallengeEvent.EventType.WaitForAnswer -> {
StateResolution(WaitingForAnswer(challengeEvent.challenge))
}
else -> defaultResolution
}
}
else -> defaultResolution
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
Expand Down Expand Up @@ -149,6 +149,10 @@ internal sealed class SignInState : State {
val action = signInActions.confirmDevice(signInEvent)
StateResolution(ConfirmingDevice(), listOf(action))
}
is SignInEvent.EventType.ReceivedChallenge -> {
val action = signInActions.initResolveChallenge(signInEvent)
StateResolution(ResolvingChallenge(oldState.challengeState), listOf(action))
}
is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception))
else -> defaultResolution
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

package com.amplifyframework.auth.cognito.featuretest.generators.testcasegenerators

import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException
import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter
import com.amplifyframework.auth.cognito.featuretest.API
import com.amplifyframework.auth.cognito.featuretest.AuthAPI
import com.amplifyframework.auth.cognito.featuretest.CognitoType
Expand Down Expand Up @@ -102,5 +104,37 @@ object ConfirmSignInTestCaseGenerator : SerializableProvider {
)
)

override val serializables: List<Any> = listOf(baseCase)
private val errorCase: FeatureTestCase
get() {
val exception = CodeMismatchException.invoke {
message = "Confirmation code entered is not correct."
}
return baseCase.copy(
description = "Test that invalid code on confirm SignIn with SMS challenge errors out",
preConditions = PreConditions(
"authconfiguration.json",
"SigningIn_SigningIn.json",
mockedResponses = listOf(
MockResponse(
CognitoType.CognitoIdentityProvider,
"respondToAuthChallenge",
ResponseType.Failure,
exception.toJsonElement()
)
)
),
validations = listOf(
ExpectationShapes.Amplify(
AuthAPI.confirmSignIn,
ResponseType.Failure,
CognitoAuthExceptionConverter.lookup(
exception,
"Confirm Sign in failed."
).toJsonElement()
)
)
)
}

override val serializables: List<Any> = listOf(baseCase, errorCase)
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ internal data class AuthStatesProxy(
type = "SignInChallengeState.WaitingForAnswer",
authChallenge = authState.challenge
)
is SignInChallengeState.Error -> TODO()
}
}
else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
package com.amplifyframework.auth.cognito.featuretest.serializers

import aws.sdk.kotlin.services.cognitoidentity.model.CognitoIdentityException
import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException
import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProviderException
import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException
import com.amplifyframework.auth.exceptions.UnknownException
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
Expand All @@ -32,14 +34,19 @@ private data class CognitoExceptionSurrogate(
val errorMessage: String?
) {
fun <T> toRealException(): T {
return when (errorType) {
val exception = when (errorType) {
NotAuthorizedException::class.java.simpleName -> NotAuthorizedException.invoke {
message = errorMessage
} as T
UnknownException::class.java.simpleName -> UnknownException(message = errorMessage ?: "") as T
CodeMismatchException::class.java.simpleName -> CodeMismatchException.invoke {
message = errorMessage
} as T
else -> {
error("Exception for $errorType not defined")
}
}
return exception
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.amplifyframework.core.Consumer
import com.google.gson.Gson
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.Exception
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import aws.sdk.kotlin.services.cognitoidentity.model.Credentials
import aws.sdk.kotlin.services.cognitoidentity.model.GetCredentialsForIdentityResponse
import aws.sdk.kotlin.services.cognitoidentity.model.GetIdResponse
import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient
import aws.sdk.kotlin.services.cognitoidentityprovider.forgetDevice
import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType
import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType
import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType
Expand Down Expand Up @@ -211,13 +210,14 @@ class CognitoMockFactory(
responseObject: JsonObject
) {
if (mockResponse.responseType == ResponseType.Failure) {
throw Json.decodeFromString(
val response = Json.decodeFromString(
when (mockResponse.type) {
CognitoType.CognitoIdentity -> CognitoIdentityExceptionSerializer
CognitoType.CognitoIdentityProvider -> CognitoIdentityProviderExceptionSerializer
},
responseObject.toString()
)
throw response
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"description": "Test that invalid code on confirm SignIn with SMS challenge errors out",
"preConditions": {
"amplify-configuration": "authconfiguration.json",
"state": "SigningIn_SigningIn.json",
"mockedResponses": [
{
"type": "cognitoIdentityProvider",
"apiName": "respondToAuthChallenge",
"responseType": "failure",
"response": {
"errorType": "CodeMismatchException",
"errorMessage": "Confirmation code entered is not correct."
}
}
]
},
"api": {
"name": "confirmSignIn",
"params": {
"challengeResponse": "000000"
},
"options": {
}
},
"validations": [
{
"type": "amplify",
"apiName": "confirmSignIn",
"responseType": "failure",
"response": {
"errorType": "CodeMismatchException",
"errorMessage": "Confirmation code entered is not correct.",
"recoverySuggestion": "Enter correct confirmation code.",
"cause": {
"errorType": "CodeMismatchException",
"errorMessage": "Confirmation code entered is not correct."
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@
],
"deviceCreateDate": {
"value": {
"seconds": 1.671733506E9,
"nanos": 9.95178E8
"seconds": 1.676485556E9,
"nanos": 7.65001E8
}
},
"deviceKey": "deviceKey",
"deviceLastAuthenticatedDate": {
"value": {
"seconds": 1.671733506E9,
"nanos": 9.95186E8
"seconds": 1.676485556E9,
"nanos": 7.65007E8
}
},
"deviceLastModifiedDate": {
"value": {
"seconds": 1.671733506E9,
"nanos": 9.95188E8
"seconds": 1.676485556E9,
"nanos": 7.65008E8
}
}
}
Expand Down
Loading