diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt index e9db425a6a..0ec2f74e29 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt @@ -232,7 +232,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { password: String?, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signIn(username, password) } + ) = enqueue(onSuccess, onError) { useCaseFactory.signIn().execute(username, password) } override fun signIn( username: String?, @@ -240,20 +240,20 @@ class AWSCognitoAuthPlugin : AuthPlugin() { options: AuthSignInOptions, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signIn(username, password, options) } + ) = enqueue(onSuccess, onError) { useCaseFactory.signIn().execute(username, password, options) } override fun confirmSignIn( challengeResponse: String, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.confirmSignIn(challengeResponse) } + ) = enqueue(onSuccess, onError) { useCaseFactory.confirmSignIn().execute(challengeResponse) } override fun confirmSignIn( challengeResponse: String, options: AuthConfirmSignInOptions, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.confirmSignIn(challengeResponse, options) } + ) = enqueue(onSuccess, onError) { useCaseFactory.confirmSignIn().execute(challengeResponse, options) } override fun signInWithSocialWebUI( provider: AuthProvider, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt index a9d60fe604..22714a3483 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt @@ -132,12 +132,11 @@ internal class AuthStateMachine( } // This function throws if the state machine is *not* in the required state -internal suspend inline fun AuthStateMachine.requireAuthenticationState() { - if (getCurrentState().authNState !is T) { - throw InvalidStateException( - "Auth State Machine is not in the required authentication state: ${T::class.simpleName}" - ) - } +internal suspend inline fun AuthStateMachine.requireAuthenticationState(): T { + val currentState = getCurrentState() + return currentState.authNState as? T ?: throw InvalidStateException( + "Auth State Machine is not in the required authentication state: ${T::class.simpleName}" + ) } // Returns the SignedInState or throws SignedOutException or InvalidStateException diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt index 0fde653075..eaaeed96d4 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt @@ -102,5 +102,7 @@ internal class CognitoAuthExceptionConverter { ) else -> UnknownException(fallbackMessage, error) } + + fun Exception.toAuthException(fallbackMessage: String) = lookup(this, fallbackMessage) } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt index bc9f2335b4..eaa0642d44 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt @@ -21,9 +21,7 @@ import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult -import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions -import com.amplifyframework.auth.options.AuthSignInOptions import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthSignInResult @@ -34,44 +32,6 @@ import kotlin.coroutines.suspendCoroutine internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuthPlugin) { - suspend fun signIn(username: String?, password: String?): AuthSignInResult = suspendCoroutine { continuation -> - delegate.signIn( - username, - password, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun signIn(username: String?, password: String?, options: AuthSignInOptions): AuthSignInResult = - suspendCoroutine { continuation -> - delegate.signIn( - username, - password, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun confirmSignIn(challengeResponse: String): AuthSignInResult = suspendCoroutine { continuation -> - delegate.confirmSignIn( - challengeResponse, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun confirmSignIn(challengeResponse: String, options: AuthConfirmSignInOptions): AuthSignInResult = - suspendCoroutine { continuation -> - delegate.confirmSignIn( - challengeResponse, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - suspend fun signInWithSocialWebUI(provider: AuthProvider, callingActivity: Activity): AuthSignInResult = suspendCoroutine { continuation -> delegate.signInWithSocialWebUI( diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt index d24a0c625d..e7a353efbd 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt @@ -18,7 +18,6 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent import androidx.annotation.WorkerThread -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType @@ -26,7 +25,6 @@ import com.amplifyframework.auth.AWSCredentials import com.amplifyframework.auth.AWSTemporaryCredentials import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthException -import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException @@ -34,19 +32,11 @@ import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoo import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException import com.amplifyframework.auth.cognito.exceptions.service.HostedUISignOutException import com.amplifyframework.auth.cognito.exceptions.service.InvalidAccountTypeException -import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException import com.amplifyframework.auth.cognito.helpers.HostedUIHelper -import com.amplifyframework.auth.cognito.helpers.UserPoolSignInHelper -import com.amplifyframework.auth.cognito.helpers.getMFASetupTypeOrNull -import com.amplifyframework.auth.cognito.helpers.getMFATypeOrNull import com.amplifyframework.auth.cognito.helpers.identityProviderName -import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions -import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult @@ -60,9 +50,7 @@ import com.amplifyframework.auth.exceptions.ServiceException import com.amplifyframework.auth.exceptions.SessionExpiredException import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.exceptions.UnknownException -import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions -import com.amplifyframework.auth.options.AuthSignInOptions import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthSignInResult @@ -77,36 +65,24 @@ import com.amplifyframework.hub.HubEvent import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.StateChangeListenerToken import com.amplifyframework.statemachine.codegen.data.AmplifyCredential -import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.FederatedToken import com.amplifyframework.statemachine.codegen.data.HostedUIErrorData import com.amplifyframework.statemachine.codegen.data.SignInData import com.amplifyframework.statemachine.codegen.data.SignInMethod import com.amplifyframework.statemachine.codegen.data.SignOutData -import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext -import com.amplifyframework.statemachine.codegen.data.challengeNameType import com.amplifyframework.statemachine.codegen.errors.SessionError import com.amplifyframework.statemachine.codegen.events.AuthEvent import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent import com.amplifyframework.statemachine.codegen.events.HostedUIEvent -import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent -import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent -import com.amplifyframework.statemachine.codegen.events.SignInEvent import com.amplifyframework.statemachine.codegen.events.SignOutEvent import com.amplifyframework.statemachine.codegen.states.AuthState import com.amplifyframework.statemachine.codegen.states.AuthenticationState import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.HostedUISignInState -import com.amplifyframework.statemachine.codegen.states.SetupTOTPState -import com.amplifyframework.statemachine.codegen.states.SignInChallengeState -import com.amplifyframework.statemachine.codegen.states.SignInState import com.amplifyframework.statemachine.codegen.states.SignOutState -import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState -import java.lang.ref.WeakReference import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.takeWhile @@ -117,9 +93,6 @@ internal class RealAWSCognitoAuthPlugin( private val authStateMachine: AuthStateMachine, private val logger: Logger ) { - - private val lastPublishedHubEventName = AtomicReference() - init { addAuthStateChangeListener() configureAuthStates() @@ -161,429 +134,6 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.state.takeWhile { it !is AuthState.Configured && it !is AuthState.Error }.collect() } - fun signIn( - username: String?, - password: String?, - onSuccess: Consumer, - onError: Consumer - ) { - signIn(username, password, AuthSignInOptions.defaults(), onSuccess, onError) - } - - fun signIn( - username: String?, - password: String?, - options: AuthSignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - authStateMachine.getCurrentState { authState -> - val signInOptions = options as? AWSCognitoAuthSignInOptions ?: AWSCognitoAuthSignInOptions.builder() - .authFlowType(configuration.authFlowType) - .build() - when (authState.authNState) { - is AuthenticationState.NotConfigured -> onError.accept( - InvalidUserPoolConfigurationException() - ) - // Continue sign in - is AuthenticationState.SignedOut, - is AuthenticationState.Configured - -> { - _signIn(username, password, signInOptions, onSuccess, onError) - } - is AuthenticationState.SignedIn -> onError.accept(SignedInException()) - is AuthenticationState.SigningIn -> { - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { authState -> - when (authState.authNState) { - is AuthenticationState.SignedOut -> { - authStateMachine.cancel(token) - _signIn(username, password, signInOptions, onSuccess, onError) - } - else -> Unit - } - }, - { - authStateMachine.send(AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn())) - } - ) - } - else -> onError.accept(InvalidStateException()) - } - } - } - - private fun _signIn( - username: String?, - password: String?, - options: AWSCognitoAuthSignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { authState -> - val authNState = authState.authNState - val authZState = authState.authZState - when { - authNState is AuthenticationState.Error -> { - authStateMachine.cancel(token) - onError.accept( - CognitoAuthExceptionConverter.lookup(authNState.exception, "Sign in failed") - ) - } - authNState is AuthenticationState.SignedIn && - authZState is AuthorizationState.SessionEstablished -> { - authStateMachine.cancel(token) - onSuccess.accept(UserPoolSignInHelper.signedInResult()) - sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) - } - authNState is AuthenticationState.SigningIn -> { - var result: AuthSignInResult? = null - try { - result = UserPoolSignInHelper.checkNextStep(authNState.signInState) - } catch (e: Exception) { - authStateMachine.cancel(token) - onError.accept(CognitoAuthExceptionConverter.lookup(e, "Sign in failed")) - } - if (result != null) { - authStateMachine.cancel(token) - onSuccess.accept(result) - } - } - } - }, - { - val signInData = when (options.authFlowType ?: configuration.authFlowType) { - AuthFlowType.USER_SRP_AUTH -> { - SignInData.SRPSignInData(username, password, options.metadata, AuthFlowType.USER_SRP_AUTH) - } - AuthFlowType.CUSTOM_AUTH, AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP -> { - SignInData.CustomAuthSignInData(username, options.metadata) - } - AuthFlowType.CUSTOM_AUTH_WITH_SRP -> { - SignInData.CustomSRPAuthSignInData(username, password, options.metadata) - } - AuthFlowType.USER_PASSWORD_AUTH -> { - SignInData.MigrationAuthSignInData( - username = username, - password = password, - metadata = options.metadata, - authFlowType = AuthFlowType.USER_PASSWORD_AUTH - ) - } - AuthFlowType.USER_AUTH -> { - when (options.preferredFirstFactor) { - AuthFactorType.PASSWORD -> { - SignInData.MigrationAuthSignInData( - username = username, - password = password, - metadata = options.metadata, - authFlowType = AuthFlowType.USER_AUTH - ) - } - AuthFactorType.PASSWORD_SRP -> { - SignInData.SRPSignInData(username, password, options.metadata, AuthFlowType.USER_AUTH) - } - else -> { - SignInData.UserAuthSignInData( - username = username, - preferredChallenge = options.preferredFirstFactor, - callingActivity = options.callingActivity, - metadata = options.metadata - ) - } - } - } - } - val event = AuthenticationEvent(AuthenticationEvent.EventType.SignInRequested(signInData)) - authStateMachine.send(event) - } - ) - } - - fun confirmSignIn( - challengeResponse: String, - onSuccess: Consumer, - onError: Consumer - ) { - confirmSignIn(challengeResponse, AuthConfirmSignInOptions.defaults(), onSuccess, onError) - } - - fun confirmSignIn( - challengeResponse: String, - options: AuthConfirmSignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - authStateMachine.getCurrentState { authState -> - val authNState = authState.authNState - val signInState = (authNState as? AuthenticationState.SigningIn)?.signInState - if (signInState is SignInState.ResolvingChallenge) { - when (signInState.challengeState) { - is SignInChallengeState.WaitingForAnswer, is SignInChallengeState.Error -> { - _confirmSignIn(signInState, challengeResponse, options, onSuccess, onError) - } - else -> { - onError.accept(InvalidStateException()) - } - } - } else if (signInState is SignInState.ResolvingTOTPSetup) { - when (signInState.setupTOTPState) { - is SetupTOTPState.WaitingForAnswer, is SetupTOTPState.Error -> { - _confirmSignIn(signInState, challengeResponse, options, onSuccess, onError) - } - - else -> onError.accept(InvalidStateException()) - } - } else if (signInState is SignInState.SigningInWithWebAuthn) { - when (signInState.webAuthnSignInState) { - is WebAuthnSignInState.Error -> _confirmSignIn( - signInState, - challengeResponse, - options, - onSuccess, - onError - ) - else -> onError.accept(InvalidStateException()) - } - } else { - onError.accept(InvalidStateException()) - } - } - } - - private fun _confirmSignIn( - signInState: SignInState, - challengeResponse: String, - options: AuthConfirmSignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { authState -> - val authNState = authState.authNState - val authZState = authState.authZState - when { - authNState is AuthenticationState.Error -> { - authStateMachine.cancel(token) - onError.accept( - CognitoAuthExceptionConverter.lookup(authNState.exception, "Confirm sign in failed") - ) - } - authNState is AuthenticationState.SignedIn && - authZState is AuthorizationState.SessionEstablished -> { - authStateMachine.cancel(token) - onSuccess.accept(UserPoolSignInHelper.signedInResult()) - sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) - } - authNState is AuthenticationState.SigningIn -> { - var result: AuthSignInResult? = null - try { - result = UserPoolSignInHelper.checkNextStep(authNState.signInState) - } catch (e: Exception) { - authStateMachine.cancel(token) - onError.accept(CognitoAuthExceptionConverter.lookup(e, "Confirm sign in failed")) - } - if (result != null) { - authStateMachine.cancel(token) - onSuccess.accept(result) - } - } - } - }, - { - val awsCognitoConfirmSignInOptions = options as? AWSCognitoAuthConfirmSignInOptions - val metadata = awsCognitoConfirmSignInOptions?.metadata ?: emptyMap() - val userAttributes = awsCognitoConfirmSignInOptions?.userAttributes ?: emptyList() - when (signInState) { - is SignInState.ResolvingChallenge -> { - val challengeState = signInState.challengeState - if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.SelectMfaType && - getMFATypeOrNull(challengeResponse) == null - ) { - val error = InvalidParameterException( - message = "Value for challengeResponse must be one of " + - "SMS_MFA, EMAIL_OTP or SOFTWARE_TOKEN_MFA" - ) - onError.accept(error) - authStateMachine.cancel(token) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - isMfaSetupSelectionChallenge(challengeState.challenge) && - getMFASetupTypeOrNull(challengeResponse) == null - ) { - val error = InvalidParameterException( - message = "Value for challengeResponse must be one of EMAIL_OTP or SOFTWARE_TOKEN_MFA" - ) - onError.accept(error) - authStateMachine.cancel(token) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && - challengeResponse == AuthFactorType.WEB_AUTHN.challengeResponse - ) { - val username = challengeState.challenge.username!! - val session = challengeState.challenge.session - val signInContext = WebAuthnSignInContext( - username = username, - callingActivity = awsCognitoConfirmSignInOptions?.callingActivity ?: WeakReference( - null - ), - session = session - ) - val event = SignInEvent(SignInEvent.EventType.InitiateWebAuthnSignIn(signInContext)) - authStateMachine.send(event) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && - challengeResponse == ChallengeNameType.Password.value - ) { - val event = SignInEvent( - SignInEvent.EventType.ReceivedChallenge( - AuthChallenge( - challengeName = ChallengeNameType.Password.value, - username = challengeState.challenge.username, - session = challengeState.challenge.session, - parameters = challengeState.challenge.parameters - ), - signInMethod = challengeState.signInMethod - ) - ) - authStateMachine.send(event) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && - challengeResponse == ChallengeNameType.PasswordSrp.value - ) { - val event = SignInEvent( - SignInEvent.EventType.ReceivedChallenge( - AuthChallenge( - challengeName = ChallengeNameType.PasswordSrp.value, - username = challengeState.challenge.username, - session = challengeState.challenge.session, - parameters = challengeState.challenge.parameters - ), - signInMethod = challengeState.signInMethod - ) - ) - authStateMachine.send(event) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.Password - ) { - val event = SignInEvent( - SignInEvent.EventType.InitiateMigrateAuth( - username = challengeState.challenge.username!!, - password = challengeResponse, - metadata = metadata, - authFlowType = AuthFlowType.USER_AUTH, - respondToAuthChallenge = AuthChallenge( - challengeName = ChallengeNameType.SelectChallenge.value, - username = challengeState.challenge.username, - session = challengeState.challenge.session!!, - parameters = null - ) - ) - ) - authStateMachine.send(event) - } else if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeNameType == ChallengeNameType.PasswordSrp - ) { - val event = SignInEvent( - SignInEvent.EventType.InitiateSignInWithSRP( - username = challengeState.challenge.username!!, - password = challengeResponse, - metadata = metadata, - authFlowType = AuthFlowType.USER_AUTH, - respondToAuthChallenge = AuthChallenge( - challengeName = ChallengeNameType.SelectChallenge.value, - username = challengeState.challenge.username, - session = challengeState.challenge.session!!, - parameters = null - ) - ) - ) - authStateMachine.send(event) - } else { - val event = SignInChallengeEvent( - SignInChallengeEvent.EventType.VerifyChallengeAnswer( - challengeResponse, - metadata, - userAttributes - ) - ) - authStateMachine.send(event) - } - } - - is SignInState.ResolvingTOTPSetup -> { - when (signInState.setupTOTPState) { - is SetupTOTPState.WaitingForAnswer -> { - val setupTOTPState = - (signInState.setupTOTPState as SetupTOTPState.WaitingForAnswer) - - val event = SetupTOTPEvent( - SetupTOTPEvent.EventType.VerifyChallengeAnswer( - challengeResponse, - setupTOTPState.signInTOTPSetupData.username, - setupTOTPState.signInTOTPSetupData.session, - awsCognitoConfirmSignInOptions?.friendlyDeviceName, - setupTOTPState.signInMethod - ) - ) - authStateMachine.send(event) - } - is SetupTOTPState.Error -> { - val username = - (signInState.setupTOTPState as SetupTOTPState.Error).username - val session = - (signInState.setupTOTPState as SetupTOTPState.Error).session - val signInMethod = - (signInState.setupTOTPState as SetupTOTPState.Error).signInMethod - - val event = SetupTOTPEvent( - SetupTOTPEvent.EventType.VerifyChallengeAnswer( - challengeResponse, - username, - session, - awsCognitoConfirmSignInOptions?.friendlyDeviceName, - signInMethod - ) - ) - authStateMachine.send(event) - } - - else -> { - onError.accept(InvalidStateException()) - authStateMachine.cancel(token) - } - } - } - - is SignInState.SigningInWithWebAuthn -> { - if (signInState.webAuthnSignInState is WebAuthnSignInState.Error && - challengeResponse == AuthFactorType.WEB_AUTHN.challengeResponse - ) { - val signInContext = (signInState.webAuthnSignInState as WebAuthnSignInState.Error).context - val event = SignInEvent(SignInEvent.EventType.InitiateWebAuthnSignIn(signInContext)) - authStateMachine.send(event) - } else { - onError.accept(InvalidStateException()) - authStateMachine.cancel(token) - } - } - - else -> { - onError.accept(InvalidStateException()) - authStateMachine.cancel(token) - } - } - } - ) - } - fun signInWithSocialWebUI( provider: AuthProvider, callingActivity: Activity, @@ -1223,9 +773,6 @@ internal class RealAWSCognitoAuthPlugin( } private fun sendHubEvent(eventName: String) { - if (lastPublishedHubEventName.get() != eventName) { - lastPublishedHubEventName.set(eventName) - Amplify.Hub.publish(HubChannel.AUTH, HubEvent.create(eventName)) - } + Amplify.Hub.publish(HubChannel.AUTH, HubEvent.create(eventName)) } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/UserPoolSignInHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/UserPoolSignInHelper.kt index 7f46e882ba..798562f8fd 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/UserPoolSignInHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/UserPoolSignInHelper.kt @@ -32,63 +32,44 @@ import com.amplifyframework.statemachine.codegen.states.SignInState import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState internal object UserPoolSignInHelper { - fun checkNextStep(signInState: SignInState): AuthSignInResult? { - when (signInState) { - is SignInState.Error -> throw signInState.exception - is SignInState.SigningInWithSRP -> { - val srpState = signInState.srpSignInState - if (srpState is SRPSignInState.Error) throw srpState.exception - } - is SignInState.SigningInWithSRPCustom -> { - val srpState = signInState.srpSignInState - if (srpState is SRPSignInState.Error) throw srpState.exception - } - // Swift has an error state for migrate auth, Android does not - is SignInState.SigningInViaMigrateAuth -> Unit - is SignInState.SigningInWithCustom -> { - val customState = signInState.customSignInState - if (customState is CustomSignInState.Error) throw customState.error - } - is SignInState.SigningInWithHostedUI -> { - val hostedUiState = signInState.hostedUISignInState - if (hostedUiState is HostedUISignInState.Error) throw hostedUiState.exception - } - is SignInState.ResolvingChallenge -> { - val challengeState = signInState.challengeState - if (challengeState is SignInChallengeState.WaitingForAnswer && challengeState.hasNewResponse) { - challengeState.hasNewResponse = false - return SignInChallengeHelper.getNextStep(challengeState.challenge) - } - if (challengeState is SignInChallengeState.Error && challengeState.hasNewResponse) { - challengeState.hasNewResponse = false - throw challengeState.exception - } - } - is SignInState.ResolvingTOTPSetup -> { - val setupTotpState = signInState.setupTOTPState - if (setupTotpState is SetupTOTPState.Error && setupTotpState.hasNewResponse) { - setupTotpState.hasNewResponse = false - throw setupTotpState.exception - } - if (setupTotpState is SetupTOTPState.WaitingForAnswer && setupTotpState.hasNewResponse) { - setupTotpState.hasNewResponse = false - return signInResult( - signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, - totpSetupDetails = setupTotpState.signInTOTPSetupData.toTotpSetupDetails() - ) - } - } - is SignInState.SigningInWithWebAuthn -> { - val webAuthnState = signInState.webAuthnSignInState - if (webAuthnState is WebAuthnSignInState.Error && webAuthnState.hasNewResponse) { - webAuthnState.hasNewResponse = false - throw webAuthnState.exception - } - } - else -> Unit + fun checkNextStep(signInState: SignInState): AuthSignInResult? = when (signInState) { + is SignInState.Error -> throw signInState.exception + is SignInState.SigningInWithSRP -> when (val srpState = signInState.srpSignInState) { + is SRPSignInState.Error -> throw srpState.exception + else -> null } - - return null + is SignInState.SigningInWithSRPCustom -> when (val srpState = signInState.srpSignInState) { + is SRPSignInState.Error -> throw srpState.exception + else -> null + } + // Swift has an error state for migrate auth, Android does not + is SignInState.SigningInViaMigrateAuth -> null + is SignInState.SigningInWithCustom -> when (val customState = signInState.customSignInState) { + is CustomSignInState.Error -> throw customState.error + else -> null + } + is SignInState.SigningInWithHostedUI -> when (val hostedUiState = signInState.hostedUISignInState) { + is HostedUISignInState.Error -> throw hostedUiState.exception + else -> null + } + is SignInState.ResolvingChallenge -> when (val challengeState = signInState.challengeState) { + is SignInChallengeState.Error -> throw challengeState.exception + is SignInChallengeState.WaitingForAnswer -> SignInChallengeHelper.getNextStep(challengeState.challenge) + else -> null + } + is SignInState.ResolvingTOTPSetup -> when (val totpState = signInState.setupTOTPState) { + is SetupTOTPState.Error -> throw totpState.exception + is SetupTOTPState.WaitingForAnswer -> signInResult( + signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + totpSetupDetails = totpState.signInTOTPSetupData.toTotpSetupDetails() + ) + else -> null + } + is SignInState.SigningInWithWebAuthn -> when (val webAuthnState = signInState.webAuthnSignInState) { + is WebAuthnSignInState.Error -> throw webAuthnState.exception + else -> null + } + else -> null } fun signInResult( diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt index 9f30998a4b..6e6f7a99fe 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt @@ -160,4 +160,11 @@ internal class AuthUseCaseFactory( fetchAuthSession = fetchAuthSession(), stateMachine = stateMachine ) + + fun signIn() = SignInUseCase( + stateMachine = stateMachine, + configuration = authEnvironment.configuration + ) + + fun confirmSignIn() = ConfirmSignInUseCase(stateMachine = stateMachine) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCase.kt index 22aff318a3..a9208f83fc 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCase.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCase.kt @@ -19,7 +19,7 @@ import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException -import com.amplifyframework.auth.cognito.helpers.UserPoolSignInHelper +import com.amplifyframework.auth.cognito.util.sendEventAndGetSignInResult import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter import com.amplifyframework.auth.result.AuthSignInResult @@ -28,12 +28,8 @@ import com.amplifyframework.statemachine.codegen.data.SignUpData import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.states.AuthState import com.amplifyframework.statemachine.codegen.states.AuthenticationState -import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.SignUpState -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.transformWhile internal class AutoSignInUseCase( @@ -43,7 +39,21 @@ internal class AutoSignInUseCase( suspend fun execute(): AuthSignInResult { val authState = waitForSignedOutState() val signUpData = getSignUpData(authState) - val result = completeAutoSignIn(signUpData) + + val signInData = SignInData.AutoSignInData( + signUpData.username, + signUpData.session, + signUpData.clientMetadata ?: mapOf(), + signUpData.userId + ) + val event = AuthenticationEvent(AuthenticationEvent.EventType.SignInRequested(signInData)) + + val result = stateMachine.sendEventAndGetSignInResult(event) + + if (result.isSignedIn) { + hubEmitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + return result } @@ -74,37 +84,4 @@ internal class AutoSignInUseCase( is SignUpState.SignedUp -> signUpState.signUpData else -> throw InvalidStateException() } - - private suspend fun completeAutoSignIn(signUpData: SignUpData): AuthSignInResult { - val signInData = SignInData.AutoSignInData( - signUpData.username, - signUpData.session, - signUpData.clientMetadata ?: mapOf(), - signUpData.userId - ) - - val result = stateMachine.state - .onSubscription { - val event = AuthenticationEvent(AuthenticationEvent.EventType.SignInRequested(signInData)) - stateMachine.send(event) - } - .drop(1) - .mapNotNull { authState -> - val authNState = authState.authNState - val authZState = authState.authZState - when { - authNState is AuthenticationState.Error -> throw authNState.exception - authNState is AuthenticationState.SigningIn -> { - UserPoolSignInHelper.checkNextStep(signInState = authNState.signInState) - } - authNState is AuthenticationState.SignedIn && - authZState is AuthorizationState.SessionEstablished -> { - hubEmitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) - UserPoolSignInHelper.signedInResult() - } - else -> null - } - }.first() - return result - } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCase.kt new file mode 100644 index 0000000000..8f468a0544 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCase.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2025 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException +import com.amplifyframework.auth.cognito.helpers.getMFASetupTypeOrNull +import com.amplifyframework.auth.cognito.helpers.getMFATypeOrNull +import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.cognito.util.sendEventAndGetSignInResult +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthConfirmSignInOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.data.challengeNameType +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent +import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.SetupTOTPState +import com.amplifyframework.statemachine.codegen.states.SignInChallengeState +import com.amplifyframework.statemachine.codegen.states.SignInState +import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState +import java.lang.ref.WeakReference + +internal class ConfirmSignInUseCase( + private val stateMachine: AuthStateMachine, + private val hubEmitter: AuthHubEventEmitter = AuthHubEventEmitter() +) { + suspend fun execute( + challengeResponse: String, + options: AuthConfirmSignInOptions = AuthConfirmSignInOptions.defaults() + ): AuthSignInResult { + val signInState = checkCanSubmitChallengeResponse() + val event = createStateMachineEvent( + signInState = signInState, + challengeResponse = challengeResponse, + options = options + ) + + val result = stateMachine.sendEventAndGetSignInResult(event) + + if (result.isSignedIn) { + hubEmitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + + return result + } + + private suspend fun checkCanSubmitChallengeResponse(): SignInState { + val currentState = stateMachine.requireAuthenticationState() + return when (val signInState = currentState.signInState) { + is SignInState.ResolvingChallenge -> when (signInState.challengeState) { + is SignInChallengeState.WaitingForAnswer, is SignInChallengeState.Error -> signInState + else -> throw InvalidStateException() + } + is SignInState.ResolvingTOTPSetup -> when (signInState.setupTOTPState) { + is SetupTOTPState.WaitingForAnswer, is SetupTOTPState.Error -> signInState + else -> throw InvalidStateException() + } + is SignInState.SigningInWithWebAuthn -> when (signInState.webAuthnSignInState) { + is WebAuthnSignInState.Error -> signInState + else -> throw InvalidStateException() + } + else -> throw InvalidStateException() + } + } + + private fun createStateMachineEvent( + signInState: SignInState, + challengeResponse: String, + options: AuthConfirmSignInOptions + ): StateMachineEvent { + val cognitoOptions = options as? AWSCognitoAuthConfirmSignInOptions + val metadata = cognitoOptions?.metadata ?: emptyMap() + val userAttributes = cognitoOptions?.userAttributes ?: emptyList() + + return when (signInState) { + is SignInState.ResolvingChallenge -> { + val challengeState = signInState.challengeState + + if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.SelectMfaType && + getMFATypeOrNull(challengeResponse) == null + ) { + throw InvalidParameterException( + message = "Value for challengeResponse must be one of SMS_MFA, EMAIL_OTP or SOFTWARE_TOKEN_MFA" + ) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + isMfaSetupSelectionChallenge(challengeState.challenge) && + getMFASetupTypeOrNull(challengeResponse) == null + ) { + throw InvalidParameterException( + message = "Value for challengeResponse must be one of EMAIL_OTP or SOFTWARE_TOKEN_MFA" + ) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && + challengeResponse == AuthFactorType.WEB_AUTHN.challengeResponse + ) { + val username = challengeState.challenge.username!! + val session = challengeState.challenge.session + val signInContext = WebAuthnSignInContext( + username = username, + callingActivity = cognitoOptions?.callingActivity ?: WeakReference(null), + session = session + ) + SignInEvent(SignInEvent.EventType.InitiateWebAuthnSignIn(signInContext)) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && + challengeResponse == ChallengeNameType.Password.value + ) { + SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.Password.value, + username = challengeState.challenge.username, + session = challengeState.challenge.session, + parameters = challengeState.challenge.parameters + ), + signInMethod = challengeState.signInMethod + ) + ) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.SelectChallenge && + challengeResponse == ChallengeNameType.PasswordSrp.value + ) { + SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.PasswordSrp.value, + username = challengeState.challenge.username, + session = challengeState.challenge.session, + parameters = challengeState.challenge.parameters + ), + signInMethod = challengeState.signInMethod + ) + ) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.Password + ) { + SignInEvent( + SignInEvent.EventType.InitiateMigrateAuth( + username = challengeState.challenge.username!!, + password = challengeResponse, + metadata = metadata, + authFlowType = AuthFlowType.USER_AUTH, + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = challengeState.challenge.username, + session = challengeState.challenge.session!!, + parameters = null + ) + ) + ) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + challengeState.challenge.challengeNameType == ChallengeNameType.PasswordSrp + ) { + SignInEvent( + SignInEvent.EventType.InitiateSignInWithSRP( + username = challengeState.challenge.username!!, + password = challengeResponse, + metadata = metadata, + authFlowType = AuthFlowType.USER_AUTH, + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = challengeState.challenge.username, + session = challengeState.challenge.session!!, + parameters = null + ) + ) + ) + } else { + SignInChallengeEvent( + SignInChallengeEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + metadata, + userAttributes + ) + ) + } + } + is SignInState.ResolvingTOTPSetup -> { + when (val totpState = signInState.setupTOTPState) { + is SetupTOTPState.WaitingForAnswer -> SetupTOTPEvent( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + totpState.signInTOTPSetupData.username, + totpState.signInTOTPSetupData.session, + cognitoOptions?.friendlyDeviceName, + totpState.signInMethod + ) + ) + is SetupTOTPState.Error -> SetupTOTPEvent( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + totpState.username, + totpState.session, + cognitoOptions?.friendlyDeviceName, + totpState.signInMethod + ) + ) + else -> throw InvalidStateException() + } + } + is SignInState.SigningInWithWebAuthn -> { + val webAuthnState = signInState.webAuthnSignInState + if (webAuthnState is WebAuthnSignInState.Error && + challengeResponse == AuthFactorType.WEB_AUTHN.challengeResponse + ) { + SignInEvent(SignInEvent.EventType.InitiateWebAuthnSignIn(webAuthnState.context)) + } else { + throw InvalidStateException() + } + } + else -> throw InvalidStateException() + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignInUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignInUseCase.kt new file mode 100644 index 0000000000..6c1c1e9c28 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignInUseCase.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2025 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter.Companion.toAuthException +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException +import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.auth.cognito.util.sendEventAndGetSignInResult +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthSignInOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.statemachine.codegen.data.SignInData +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull + +internal class SignInUseCase( + private val stateMachine: AuthStateMachine, + private val configuration: AuthConfiguration, + private val hubEmitter: AuthHubEventEmitter = AuthHubEventEmitter() +) { + suspend fun execute( + username: String?, + password: String?, + options: AuthSignInOptions = AuthSignInOptions.defaults() + ): AuthSignInResult { + val signInData = getSignInData(username = username, password = password, options = options) + + // Make sure we can sign in + waitForStateThatAllowsSignIn() + + val event = AuthenticationEvent(AuthenticationEvent.EventType.SignInRequested(signInData)) + val result = stateMachine.sendEventAndGetSignInResult(event) + + if (result.isSignedIn) { + hubEmitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + + return result + } + + private suspend fun waitForStateThatAllowsSignIn(): AuthState { + val authState = stateMachine.state.mapNotNull { authState -> + when (val authNState = authState.authNState) { + is AuthenticationState.NotConfigured -> throw InvalidUserPoolConfigurationException() + is AuthenticationState.SignedOut, is AuthenticationState.Configured -> authState + is AuthenticationState.SignedIn -> throw SignedInException() + is AuthenticationState.SigningOut -> null + is AuthenticationState.SigningIn -> { + // Cancel the sign in + stateMachine.send(AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn())) + null + } + is AuthenticationState.Error -> throw authNState.exception.toAuthException("Sign in failed.") + else -> throw InvalidStateException() + } + }.first() + return authState + } + + private fun getSignInData(username: String?, password: String?, options: AuthSignInOptions): SignInData { + val cognitoOptions = options as? AWSCognitoAuthSignInOptions ?: AWSCognitoAuthSignInOptions.builder().build() + return when (cognitoOptions.authFlowType ?: configuration.authFlowType) { + AuthFlowType.USER_SRP_AUTH -> SignInData.SRPSignInData( + username = username, + password = password, + metadata = cognitoOptions.metadata, + authFlowType = AuthFlowType.USER_SRP_AUTH + ) + AuthFlowType.CUSTOM_AUTH, AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP -> SignInData.CustomAuthSignInData( + username = username, + metadata = cognitoOptions.metadata + ) + AuthFlowType.CUSTOM_AUTH_WITH_SRP -> SignInData.CustomSRPAuthSignInData( + username = username, + password = password, + metadata = cognitoOptions.metadata + ) + AuthFlowType.USER_PASSWORD_AUTH -> SignInData.MigrationAuthSignInData( + username = username, + password = password, + metadata = cognitoOptions.metadata, + authFlowType = AuthFlowType.USER_PASSWORD_AUTH + ) + AuthFlowType.USER_AUTH -> when (cognitoOptions.preferredFirstFactor) { + AuthFactorType.PASSWORD -> SignInData.MigrationAuthSignInData( + username = username, + password = password, + metadata = cognitoOptions.metadata, + authFlowType = AuthFlowType.USER_AUTH + ) + AuthFactorType.PASSWORD_SRP -> SignInData.SRPSignInData( + username = username, + password = password, + metadata = cognitoOptions.metadata, + authFlowType = AuthFlowType.USER_AUTH + ) + else -> SignInData.UserAuthSignInData( + username = username, + preferredChallenge = cognitoOptions.preferredFirstFactor, + callingActivity = cognitoOptions.callingActivity, + metadata = cognitoOptions.metadata + ) + } + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/util/StateMachineExtensions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/util/StateMachineExtensions.kt new file mode 100644 index 0000000000..ea880bbd4b --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/util/StateMachineExtensions.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.util + +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.helpers.UserPoolSignInHelper +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onSubscription + +internal suspend fun AuthStateMachine.sendEventAndGetSignInResult(event: StateMachineEvent) = state + .onSubscription { send(event) } + .drop(1) // Ignore current state + .mapNotNull { authState -> + val authNState = authState.authNState + val authZState = authState.authZState + when { + authNState is AuthenticationState.Error -> throw authNState.exception + authNState is AuthenticationState.SigningIn -> UserPoolSignInHelper.checkNextStep( + signInState = authNState.signInState + ) + authNState is AuthenticationState.SignedIn && + authZState is AuthorizationState.SessionEstablished -> UserPoolSignInHelper.signedInResult() + else -> null + } + }.first() diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt index 8dff1b33c7..1c174615bc 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt @@ -30,9 +30,7 @@ internal sealed class SetupTOTPState : State { val signInTOTPSetupData: SignInTOTPSetupData, val challengeParams: Map?, val signInMethod: SignInMethod - ) : SetupTOTPState() { - var hasNewResponse = true - } + ) : SetupTOTPState() data class Verifying(val id: String = "") : SetupTOTPState() data class RespondingToAuthChallenge(val id: String = "") : SetupTOTPState() data class Success(val id: String = "") : SetupTOTPState() @@ -41,9 +39,7 @@ internal sealed class SetupTOTPState : State { val username: String, val session: String?, val signInMethod: SignInMethod - ) : SetupTOTPState() { - var hasNewResponse = true - } + ) : SetupTOTPState() class Resolver(private val setupTOTPActions: SetupTOTPActions) : StateMachineResolver { override val defaultState = NotStarted("default") diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt index e17b17e3bc..943f6cbde1 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt @@ -29,9 +29,7 @@ internal sealed class SignInChallengeState : State { data class WaitingForAnswer( val challenge: AuthChallenge, val signInMethod: SignInMethod - ) : SignInChallengeState() { - var hasNewResponse = true - } + ) : SignInChallengeState() data class Verifying( val id: String = "", val signInMethod: SignInMethod @@ -41,9 +39,7 @@ internal sealed class SignInChallengeState : State { val exception: Exception, val challenge: AuthChallenge, val signInMethod: SignInMethod - ) : SignInChallengeState() { - var hasNewResponse = true - } + ) : SignInChallengeState() class Resolver(private val challengeActions: SignInChallengeActions) : StateMachineResolver { override val defaultState: SignInChallengeState = NotStarted() diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt index 4cbf3eb8c7..2d08b9d8ca 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt @@ -34,9 +34,7 @@ internal sealed class SignUpState : State { data class Error( val signUpData: SignUpData, val exception: Exception - ) : SignUpState() { - var hasNewResponse = true - } + ) : SignUpState() class Resolver(private val signUpActions: SignUpActions) : StateMachineResolver { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt index c1d67ebcb2..be8267553d 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt @@ -31,9 +31,7 @@ internal sealed class WebAuthnSignInState : State { data class AssertingCredentials(val id: String = "") : WebAuthnSignInState() data class VerifyingCredentialsAndSigningIn(val id: String = "") : WebAuthnSignInState() data class SignedIn(val id: String = "") : WebAuthnSignInState() - data class Error(val exception: Exception, val context: WebAuthnSignInContext) : WebAuthnSignInState() { - var hasNewResponse = true - } + data class Error(val exception: Exception, val context: WebAuthnSignInContext) : WebAuthnSignInState() class Resolver(private val actions: WebAuthnSignInActions, private val signInActions: SignInActions) : StateMachineResolver { diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt index bdf748d05b..da2bc31913 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt @@ -176,9 +176,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.signIn() + authPlugin.signIn(expectedUsername, expectedPassword, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signIn(expectedUsername, expectedPassword, any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedUsername, expectedPassword) } } @Test @@ -189,17 +191,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.signIn() + authPlugin.signIn(expectedUsername, expectedPassword, expectedOptions, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.signIn( - expectedUsername, - expectedPassword, - expectedOptions, - any(), - any() - ) - } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedUsername, expectedPassword, expectedOptions) } } @Test @@ -208,9 +204,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.confirmSignIn() + authPlugin.confirmSignIn(expectedChallengeResponse, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.confirmSignIn(expectedChallengeResponse, any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedChallengeResponse) } } @Test @@ -220,11 +218,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.confirmSignIn() + authPlugin.confirmSignIn(expectedConfirmationCode, expectedOptions, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.confirmSignIn(expectedConfirmationCode, expectedOptions, any(), any()) - } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedConfirmationCode, expectedOptions) } } @Test diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt index 0ec803337d..41feb937ca 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt @@ -26,6 +26,8 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundExcepti import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator.DUMMY_TOKEN import com.amplifyframework.auth.cognito.helpers.AuthHelper +import com.amplifyframework.auth.cognito.usecases.SignInUseCase +import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.core.Consumer import com.amplifyframework.logging.Logger @@ -37,6 +39,7 @@ import com.amplifyframework.statemachine.codegen.states.AuthState import com.amplifyframework.statemachine.codegen.states.AuthenticationState import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.SignUpState +import io.kotest.assertions.throwables.shouldThrow import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every @@ -134,6 +137,11 @@ class AuthValidationTest { logger = logger ) + private val signInUseCase = SignInUseCase( + stateMachine = stateMachine, + configuration = configuration + ) + private val mainThreadSurrogate = newSingleThreadContext("Main thread") //region Setup/Teardown @@ -200,7 +208,7 @@ class AuthValidationTest { signIn(USERNAME_1, PASSWORD_1) signOut() assertSignedOut() - assertFails { signIn(USERNAME_2, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_2, INCORRECT_PASSWORD) } } // SPR 5 @@ -210,7 +218,7 @@ class AuthValidationTest { signIn(USERNAME_1, PASSWORD_1) signOut() assertSignedOut() - assertFails { signIn(INVALID_USERNAME, PASSWORD_1) } + shouldThrow { signIn(INVALID_USERNAME, PASSWORD_1) } } //endregion @@ -245,7 +253,7 @@ class AuthValidationTest { @Test fun `SRP sign in existing user with correct password, Hosted UI sign in`() { signIn(USERNAME_1, PASSWORD_1) - assertFails { signInHostedUi() } + shouldThrow { signInHostedUi() } assertSignedInAs(USERNAME_1) } @@ -254,7 +262,7 @@ class AuthValidationTest { @Test fun `Hosted UI sign in, SRP sign in existing user with correct password`() { signInHostedUi() - assertFails { signIn(USERNAME_1, PASSWORD_1) } + shouldThrow { signIn(USERNAME_1, PASSWORD_1) } assertSignedInAs(USERNAME_1) } @@ -263,7 +271,7 @@ class AuthValidationTest { @Test fun `Hosted UI sign in, SRP sign in existing user with incorrect password`() { signInHostedUi() - assertFails { signIn(USERNAME_1, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_1, INCORRECT_PASSWORD) } assertSignedInAs(USERNAME_1) } @@ -272,7 +280,7 @@ class AuthValidationTest { @Test fun `Hosted UI sign in, SRP sign in non-existent user`() { signInHostedUi() - assertFails { signIn(INVALID_USERNAME, PASSWORD_1) } + shouldThrow { signIn(INVALID_USERNAME, PASSWORD_1) } } // SRP/Hosted 5 @@ -301,7 +309,7 @@ class AuthValidationTest { fun `Hosted UI sign in, Hosted UI sign out, SRP sign in existing user with incorrect password`() { signInHostedUi() signOutHostedUi() - assertFails { signIn(USERNAME_1, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_1, INCORRECT_PASSWORD) } } // SRP/Hosted 8 @@ -310,7 +318,7 @@ class AuthValidationTest { fun `Hosted UI sign in, Hosted UI sign out, SRP sign in non-existent user`() { signInHostedUi() signOutHostedUi() - assertFails { signIn(INVALID_USERNAME, PASSWORD_1) } + shouldThrow { signIn(INVALID_USERNAME, PASSWORD_1) } } // SRP/Hosted 9 @@ -335,7 +343,7 @@ class AuthValidationTest { signOutHostedUi() signIn(USERNAME_1, PASSWORD_1) signOut() - assertFails { signIn(USERNAME_1, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_1, INCORRECT_PASSWORD) } } // SRP/Hosted 11 @@ -361,7 +369,7 @@ class AuthValidationTest { signOutHostedUi() signIn(USERNAME_1, PASSWORD_1) signOut() - assertFails { signIn(USERNAME_2, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_2, INCORRECT_PASSWORD) } } // SRP/Hosted 13 @@ -373,7 +381,7 @@ class AuthValidationTest { signOutHostedUi() signIn(USERNAME_1, PASSWORD_1) signOut() - assertFails { signIn(INVALID_USERNAME, PASSWORD_1) } + shouldThrow { signIn(INVALID_USERNAME, PASSWORD_1) } } // SRP/Hosted 14 @@ -397,7 +405,7 @@ class AuthValidationTest { signOut() signInHostedUi() signOutHostedUi() - assertFails { signIn(USERNAME_1, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_1, INCORRECT_PASSWORD) } } // SRP/Hosted 16 @@ -421,7 +429,7 @@ class AuthValidationTest { signOut() signInHostedUi() signOutHostedUi() - assertFails { signIn(USERNAME_2, INCORRECT_PASSWORD) } + shouldThrow { signIn(USERNAME_2, INCORRECT_PASSWORD) } } // SRP/Hosted 18 @@ -433,7 +441,7 @@ class AuthValidationTest { signOut() signInHostedUi() signOutHostedUi() - assertFails { signIn(INVALID_USERNAME, PASSWORD_1) } + shouldThrow { signIn(INVALID_USERNAME, PASSWORD_1) } } //endregion @@ -448,8 +456,8 @@ class AuthValidationTest { setupMockResponseForSuccessfulSrp(username) } - return blockForResult { success, error -> - plugin.signIn(username, password, success, error) + return runBlocking { + signInUseCase.execute(username, password) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt index 5edebc48ce..7aba26412f 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt @@ -23,9 +23,17 @@ import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.result.step.AuthNextSignInStep import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.CognitoUserPoolTokens +import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.SignInMethod import com.amplifyframework.statemachine.codegen.data.SignedInData +import com.amplifyframework.statemachine.codegen.data.SignedOutData +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import io.mockk.every +import io.mockk.mockk import java.util.Date fun mockWebAuthnCredentialDescription( @@ -71,3 +79,25 @@ internal fun mockSignedInData( signInMethod = signInMethod, cognitoUserPoolTokens = cognitoUserPoolTokens ) + +internal fun mockAuthState( + authenticationState: AuthenticationState = AuthenticationState.NotConfigured(), + authorizationState: AuthorizationState = AuthorizationState.NotConfigured() +): AuthState = mockk { + every { authNState } returns authenticationState + every { authZState } returns authorizationState +} + +internal fun mockSignedOutState(signedOutData: SignedOutData = mockk()) = mockAuthState( + AuthenticationState.SignedOut(signedOutData), + AuthorizationState.Configured() +) + +internal fun mockSignedInState( + signedInData: SignedInData = mockk(), + deviceMetadata: DeviceMetadata = mockk(), + amplifyCredential: AmplifyCredential = mockk() +) = mockAuthState( + AuthenticationState.SignedIn(signedInData, deviceMetadata), + AuthorizationState.SessionEstablished(amplifyCredential) +) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt index 6a5e57779b..9abb5f68cd 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt @@ -18,13 +18,8 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.AuthSession -import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions -import com.amplifyframework.auth.cognito.options.AuthFlowType -import com.amplifyframework.auth.options.AuthSignInOptions -import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.core.Consumer import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.codegen.data.AmplifyCredential @@ -160,71 +155,6 @@ class RealAWSCognitoAuthPluginTest { verify(exactly = 0) { onSuccess.accept(any()) } } - @Test - fun testCustomSignInWithSRPSucceedsWithChallenge() { - // GIVEN - val onSuccess = mockk>() - val onError = mockk>(relaxed = true) - - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - - // WHEN - plugin.signIn( - "username", - "password", - AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.CUSTOM_AUTH_WITH_SRP).build(), - onSuccess, - onError - ) - - // THEN - verify(exactly = 0) { onSuccess.accept(any()) } - } - - @Test - fun testSignInFailsIfNotConfigured() { - // GIVEN - val expectedAuthError = InvalidUserPoolConfigurationException() - val onSuccess = mockk>() - val onError = ConsumerWithLatch(expect = expectedAuthError) - - setupCurrentAuthState(authNState = AuthenticationState.NotConfigured()) - - coEvery { authConfiguration.authFlowType } returns AuthFlowType.USER_SRP_AUTH - // WHEN - plugin.signIn("user", "password", AuthSignInOptions.defaults(), onSuccess, onError) - - // THEN - onError.shouldBeCalled() - verify(exactly = 0) { onSuccess.accept(any()) } - } - - @Test - fun testSignInFailsIfAlreadySignedIn() { - // GIVEN - val onError = ConsumerWithLatch() - coEvery { authConfiguration.authFlowType } returns AuthFlowType.USER_SRP_AUTH - - setupCurrentAuthState( - authNState = AuthenticationState.SignedIn( - SignedInData( - "userId", - "user", - Date(), - SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_SRP_AUTH), - CognitoUserPoolTokens("", "", "", 0) - ), - mockk() - ) - ) - - // WHEN - plugin.signIn("user", "password", AuthSignInOptions.defaults(), mockk(), onError) - - // THEN - onError.shouldBeCalled() - } - private fun setupCurrentAuthState(authNState: AuthenticationState? = null, authZState: AuthorizationState? = null) { val currentAuthState = mockk { every { this@mockk.authNState } returns authNState diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt index c5adef50f6..0a6ddf2ffd 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt @@ -109,7 +109,7 @@ internal data class AuthStatesProxy( "AuthorizationState.SigningIn" -> AuthorizationState.SigningIn() as T "SignInState.ResolvingChallenge" -> SignInState.ResolvingChallenge(signInChallengeState) as T "SignInChallengeState.WaitingForAnswer" -> authChallenge?.let { - SignInChallengeState.WaitingForAnswer(it, signInMethod!!).apply { hasNewResponse = false } + SignInChallengeState.WaitingForAnswer(it, signInMethod!!) } as T else -> { error("Cannot get real type!") diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/testUtil/EventMatchers.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/testUtil/EventMatchers.kt index 84cdf998dc..52feb2da17 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/testUtil/EventMatchers.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/testUtil/EventMatchers.kt @@ -18,6 +18,8 @@ package com.amplifyframework.auth.cognito.testUtil import com.amplifyframework.statemachine.StateMachineEvent import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent +import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent import com.amplifyframework.statemachine.codegen.events.SignUpEvent import com.amplifyframework.statemachine.codegen.events.WebAuthnEvent @@ -64,3 +66,19 @@ internal inline fun MockKVerificationSco val type = event.eventType.shouldBeInstanceOf() assertions(type) } + +internal inline fun MockKVerificationScope.withChallengeEvent( + noinline assertions: MockKAssertScope.(T) -> Unit = { } +) = withArg { + val event = it.shouldBeInstanceOf() + val type = event.eventType.shouldBeInstanceOf() + assertions(type) +} + +internal inline fun MockKVerificationScope.withSetupTotpEvent( + noinline assertions: MockKAssertScope.(T) -> Unit = { } +) = withArg { + val event = it.shouldBeInstanceOf() + val type = event.eventType.shouldBeInstanceOf() + assertions(type) +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCaseTest.kt index 99081ae6bb..87c2fbfae1 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCaseTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AutoSignInUseCaseTest.kt @@ -18,6 +18,7 @@ package com.amplifyframework.auth.cognito.usecases import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException +import com.amplifyframework.auth.cognito.mockSignedInState import com.amplifyframework.auth.cognito.testUtil.withAuthEvent import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter @@ -37,7 +38,6 @@ import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -49,7 +49,6 @@ class AutoSignInUseCaseTest { private val stateMachine: AuthStateMachine = mockk { justRun { send(any()) } every { state } returns stateFlow - every { stateTransitions } returns stateFlow.drop(1) } private val hubEmitter: AuthHubEventEmitter = mockk(relaxed = true) @@ -128,10 +127,7 @@ class AutoSignInUseCaseTest { stateFlow.value = authState(AuthenticationState.SigningIn()) runCurrent() - stateFlow.value = authState( - AuthenticationState.SignedIn(mockk(), mockk()), - AuthorizationState.SessionEstablished(mockk()) - ) + stateFlow.value = mockSignedInState() val result = deferred.await() @@ -152,10 +148,7 @@ class AutoSignInUseCaseTest { stateFlow.value = authState(AuthenticationState.SigningIn()) runCurrent() - stateFlow.value = authState( - AuthenticationState.SignedIn(mockk(), mockk()), - AuthorizationState.SessionEstablished(mockk()) - ) + stateFlow.value = mockSignedInState() deferred.await() diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCaseTest.kt new file mode 100644 index 0000000000..d443b8e245 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ConfirmSignInUseCaseTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2025 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException +import com.amplifyframework.auth.cognito.mockAuthState +import com.amplifyframework.auth.cognito.mockSignedInState +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions +import com.amplifyframework.auth.cognito.testUtil.withChallengeEvent +import com.amplifyframework.auth.cognito.testUtil.withSetupTotpEvent +import com.amplifyframework.auth.cognito.testUtil.withSignInEvent +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthConfirmSignInOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent +import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.SetupTOTPState +import com.amplifyframework.statemachine.codegen.states.SignInChallengeState +import com.amplifyframework.statemachine.codegen.states.SignInState +import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import java.lang.ref.WeakReference +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ConfirmSignInUseCaseTest { + private val stateFlow = MutableStateFlow(mockAuthState()) + private val stateMachine: AuthStateMachine = mockk { + justRun { send(any()) } + every { state } returns stateFlow + coEvery { getCurrentState() } answers { stateFlow.value } + } + private val emitter: AuthHubEventEmitter = mockk(relaxed = true) + + private val useCase = ConfirmSignInUseCase(stateMachine = stateMachine, hubEmitter = emitter) + + @Test + fun `fails with invalid state when not signing in`() = runTest { + stateFlow.value = mockAuthState(mockk()) + + shouldThrow { + useCase.execute("123456", AuthConfirmSignInOptions.defaults()) + } + } + + @Test + fun `fails with invalid state when challenge state is not waiting`() = runTest { + val challengeState = mockk() + val signInState = SignInState.ResolvingChallenge(challengeState) + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(signInState)) + + shouldThrow { + useCase.execute("123456", AuthConfirmSignInOptions.defaults()) + } + } + + @Test + fun `fails with invalid parameter for invalid MFA type selection`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SelectMfaType.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(signInState)) + + shouldThrow { + useCase.execute("INVALID_MFA", AuthConfirmSignInOptions.defaults()) + } + } + + @Test + fun `sends challenge verification event for SMS MFA`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SmsMfa.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(signInState)) + + launch { + useCase.execute("123456", AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withChallengeEvent { + it.answer shouldBe "123456" + } + ) + } + } + + @Test + fun `sends WebAuthn sign in event for WebAuthn challenge selection`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + val authState = AuthenticationState.SigningIn(signInState) + stateFlow.value = mockAuthState(authState) + + launch { + useCase.execute(AuthFactorType.WEB_AUTHN.challengeResponse, AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withSignInEvent { + it.signInContext.username shouldBe "user" + } + ) + } + } + + @Test + fun `sends password challenge event for password selection`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + val authState = AuthenticationState.SigningIn(signInState) + stateFlow.value = mockAuthState(authState) + + launch { + useCase.execute(ChallengeNameType.Password.value, AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withSignInEvent { + it.challenge.challengeName shouldBe ChallengeNameType.Password.value + } + ) + } + } + + @Test + fun `sends migrate auth event for password challenge`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.Password.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + val authState = AuthenticationState.SigningIn(signInState) + stateFlow.value = mockAuthState(authState) + + launch { + useCase.execute("password123", AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withSignInEvent { + it.username shouldBe "user" + it.password shouldBe "password123" + } + ) + } + } + + @Test + fun `sends TOTP verification event for TOTP setup`() = runTest { + val totpData = SignInTOTPSetupData(secretCode = "secret", username = "user", session = "session") + val totpState = SetupTOTPState.WaitingForAnswer(totpData, emptyMap(), mockk()) + val signInState = SignInState.ResolvingTOTPSetup(totpState) + val authState = AuthenticationState.SigningIn(signInState) + stateFlow.value = mockAuthState(authState) + + val options = AWSCognitoAuthConfirmSignInOptions.builder() + .friendlyDeviceName("MyDevice") + .build() + + launch { + useCase.execute("123456", options) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withSetupTotpEvent { + it.answer shouldBe "123456" + it.friendlyDeviceName shouldBe "MyDevice" + it.username shouldBe "user" + } + ) + } + } + + @Test + fun `sends WebAuthn retry event for WebAuthn error state`() = runTest { + val context = WebAuthnSignInContext("user", WeakReference(null), "session") + val webAuthnState = WebAuthnSignInState.Error(mockk(), context) + val signInState = SignInState.SigningInWithWebAuthn(webAuthnState) + val authState = AuthenticationState.SigningIn(signInState) + stateFlow.value = mockAuthState(authState) + + launch { + useCase.execute(AuthFactorType.WEB_AUTHN.challengeResponse, AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withSignInEvent { + it.signInContext.username shouldBe "user" + } + ) + } + } + + @Test + fun `returns successful sign in result`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SmsMfa.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(signInState)) + + val deferred = async { + useCase.execute("123456", AuthConfirmSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + val result = deferred.await() + result.isSignedIn shouldBe true + result.nextStep.signInStep shouldBe AuthSignInStep.DONE + } + + @Test + fun `emits signed in hub event`() = runTest { + val challenge = AuthChallenge( + challengeName = ChallengeNameType.SmsMfa.value, + username = "user", + session = "session", + parameters = emptyMap() + ) + val challengeState = SignInChallengeState.WaitingForAnswer(challenge, mockk()) + val signInState = SignInState.ResolvingChallenge(challengeState) + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(signInState)) + + val deferred = async { + useCase.execute("123456") + } + + runCurrent() + stateFlow.value = mockSignedInState() + deferred.await() + + verify { + emitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignInUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignInUseCaseTest.kt new file mode 100644 index 0000000000..655918520b --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignInUseCaseTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2025 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException +import com.amplifyframework.auth.cognito.mockAuthState +import com.amplifyframework.auth.cognito.mockSignedInState +import com.amplifyframework.auth.cognito.mockSignedOutState +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.auth.cognito.testUtil.withAuthEvent +import com.amplifyframework.auth.options.AuthSignInOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.codegen.data.SignInData +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class SignInUseCaseTest { + private val stateFlow = MutableStateFlow(mockSignedOutState()) + private val stateMachine: AuthStateMachine = mockk { + justRun { send(any()) } + every { state } returns stateFlow + coEvery { getCurrentState() } answers { stateFlow.value } + } + private val configuration: AuthConfiguration = mockk { + every { authFlowType } returns AuthFlowType.USER_SRP_AUTH + } + private val emitter: AuthHubEventEmitter = mockk(relaxed = true) + + private val useCase = SignInUseCase( + stateMachine = stateMachine, + configuration = configuration, + hubEmitter = emitter + ) + + @Test + fun `fails if not configured`() = runTest { + val expectedAuthError = InvalidUserPoolConfigurationException() + stateFlow.value = mockAuthState(AuthenticationState.NotConfigured()) + + shouldThrowAny { + useCase.execute("user", "password", AuthSignInOptions.defaults()) + } shouldBe expectedAuthError + } + + @Test + fun `fails if authentication error occurs`() = runTest { + val exception = AuthException("test", "test") + stateFlow.value = mockAuthState(AuthenticationState.Error(exception)) + + shouldThrowAny { + useCase.execute("user", "password", AuthSignInOptions.defaults()) + } shouldBe exception + } + + @Test + fun `cancels existing sign in and proceeds`() = runTest { + stateFlow.value = mockAuthState(AuthenticationState.SigningIn(mockk())) + + launch { + useCase.execute("user", "password", AuthSignInOptions.defaults()) + } + + runCurrent() + coVerify { + stateMachine.send(match { it.eventType is AuthenticationEvent.EventType.CancelSignIn }) + } + + stateFlow.value = mockSignedOutState() + runCurrent() + stateFlow.value = mockSignedInState() + } + + @Test + fun `sends SRP sign in event for USER_SRP_AUTH flow`() = runTest { + stateFlow.value = mockAuthState(AuthenticationState.SignedOut(mockk())) + + launch { + useCase.execute("user", "password", AuthSignInOptions.defaults()) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withArg { event -> + val signInData = (event.eventType as AuthenticationEvent.EventType.SignInRequested).signInData + signInData.shouldBeInstanceOf() + signInData.username shouldBe "user" + signInData.password shouldBe "password" + } + ) + } + } + + @Test + fun `sends custom auth event for CUSTOM_AUTH flow`() = runTest { + val options = AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.CUSTOM_AUTH).build() + stateFlow.value = mockAuthState(AuthenticationState.SignedOut(mockk())) + + launch { + useCase.execute("user", null, options) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withArg { event -> + val signInData = (event.eventType as AuthenticationEvent.EventType.SignInRequested).signInData + signInData.shouldBeInstanceOf() + signInData.username shouldBe "user" + } + ) + } + } + + @Test + fun `sends user auth event for USER_AUTH flow with preferred factor`() = runTest { + val options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.EMAIL_OTP) + .build() + stateFlow.value = mockAuthState(AuthenticationState.SignedOut(mockk())) + + launch { + useCase.execute("user", null, options) + } + + runCurrent() + stateFlow.value = mockSignedInState() + + coVerify { + stateMachine.send( + withAuthEvent { + val signInData = it.signInData as SignInData.UserAuthSignInData + signInData.username shouldBe "user" + signInData.preferredChallenge shouldBe AuthFactorType.EMAIL_OTP + } + ) + } + } + + @Test + fun `returns successful sign in result`() = runTest { + val deferred = async { + useCase.execute("user", "password") + } + + runCurrent() + stateFlow.value = mockSignedInState() + + val result = deferred.await() + result.isSignedIn shouldBe true + result.nextStep.signInStep shouldBe AuthSignInStep.DONE + } + + @Test + fun `emits signed in event to auth hub`() = runTest { + val deferred = async { useCase.execute("user", "password") } + + runCurrent() + stateFlow.value = mockSignedInState() + deferred.await() + + verify { + emitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + } +}