From 41d8d99dee42ed0eeb264092a9218a555a090c6e Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Fri, 22 Nov 2024 13:59:22 -0400 Subject: [PATCH] Add Passwordless features to Amplify --- aws-auth-cognito/api/aws-auth-cognito.api | 85 +- aws-auth-cognito/build.gradle.kts | 5 + aws-auth-cognito/consumer-rules.pro | 5 + .../AWSCognitoAuthPluginEmailMFATests.kt | 7 +- .../AWSCognitoAuthPluginUserAuthTests.kt | 668 ++++++++++++++++ .../auth/cognito/AWSCognitoAuthPlugin.kt | 66 +- .../auth/cognito/AuthEnvironment.kt | 33 +- .../auth/cognito/AuthStateMachine.kt | 28 +- .../cognito/CognitoAuthExceptionConverter.kt | 88 ++- .../auth/cognito/KotlinAuthFacadeInternal.kt | 11 +- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 738 +++++++++++------- .../actions/AuthenticationCognitoActions.kt | 36 +- .../actions/MigrateAuthCognitoActions.kt | 87 ++- .../auth/cognito/actions/SRPCognitoActions.kt | 134 +++- .../actions/SignInChallengeCognitoActions.kt | 15 +- .../cognito/actions/SignInCognitoActions.kt | 97 ++- .../cognito/actions/SignUpCognitoActions.kt | 173 ++++ .../actions/UserAuthSignInCognitoActions.kt | 135 ++++ .../actions/WebAuthnSignInCognitoActions.kt | 119 +++ .../service/UserCancelledException.kt | 5 +- .../service/WebAuthnNotEnabledException.kt | 28 + ...ebAuthnCredentialAlreadyExistsException.kt | 26 + .../webauthn/WebAuthnFailedException.kt | 37 + .../webauthn/WebAuthnNotSupportedException.kt | 27 + .../webauthn/WebAuthnRpMismatchException.kt | 32 + .../cognito/helpers/AuthFactorTypeHelper.kt | 20 + .../cognito/helpers/AuthFlowTypeHelper.kt | 27 + .../auth/cognito/helpers/AuthLogger.kt | 25 + .../auth/cognito/helpers/FlowExtensions.kt | 25 + .../auth/cognito/helpers/MFAHelper.kt | 11 +- .../cognito/helpers/SignInChallengeHelper.kt | 218 ++++-- .../auth/cognito/helpers/WebAuthnHelper.kt | 157 ++++ .../AWSCognitoAuthConfirmSignInOptions.kt | 31 +- ...gnitoAuthListWebAuthnCredentialsOptions.kt | 104 +++ .../options/AWSCognitoAuthSignInOptions.java | 85 +- .../auth/cognito/options/AuthFlowType.java | 8 +- ...ognitoAuthListWebAuthnCredentialsResult.kt | 38 + .../AssociateWebAuthnCredentialUseCase.kt | 72 ++ .../cognito/usecases/AuthUseCaseFactory.kt | 50 ++ .../DeleteWebAuthnCredentialUseCase.kt | 41 + .../usecases/FetchAuthSessionUseCase.kt | 37 + .../ListWebAuthnCredentialsUseCase.kt | 61 ++ .../statemachine/StateMachine.kt | 75 +- .../codegen/actions/SignInActions.kt | 2 + .../codegen/actions/SignUpActions.kt | 24 + .../codegen/actions/UserAuthSignInActions.kt | 23 + .../codegen/actions/WebAuthnSignInActions.kt | 25 + .../codegen/data/AuthChallenge.kt | 23 +- .../codegen/data/ChallengeParameter.kt | 27 + .../statemachine/codegen/data/SignInData.kt | 25 +- .../statemachine/codegen/data/SignInMethod.kt | 1 + .../codegen/data/SignInTOTPSetupData.kt | 14 +- .../statemachine/codegen/data/SignUpData.kt | 36 + .../codegen/data/WebAuthnSignInContext.kt | 45 ++ .../statemachine/codegen/events/SRPEvent.kt | 6 +- .../codegen/events/SignInEvent.kt | 38 +- .../codegen/events/SignUpEvent.kt | 45 ++ .../codegen/events/WebAuthnEvent.kt | 33 + .../statemachine/codegen/states/AuthState.kt | 21 +- .../codegen/states/AuthenticationState.kt | 7 + .../codegen/states/AuthorizationState.kt | 9 + .../codegen/states/SignInChallengeState.kt | 19 + .../codegen/states/SignInState.kt | 64 +- .../codegen/states/SignUpState.kt | 146 ++++ .../codegen/states/WebAuthnSignInState.kt | 96 +++ .../statemachine/util/MaskUtil.kt | 28 + .../auth/cognito/AWSCognitoAuthPluginTest.kt | 69 ++ .../auth/cognito/AuthValidationTest.kt | 4 +- .../amplifyframework/auth/cognito/MockData.kt | 54 ++ .../cognito/RealAWSCognitoAuthPluginTest.kt | 303 +++---- .../auth/cognito/StateTransitionTestBase.kt | 16 +- .../auth/cognito/StateTransitionTests.kt | 18 +- .../actions/AutoSignInCognitoActionsTest.kt | 148 ++++ .../actions/MigrateAuthCognitoActionsTest.kt | 299 +++++++ .../cognito/actions/SRPCognitoActionsTest.kt | 478 ++++++++++++ .../SignInChallengeCognitoActionsTest.kt | 110 ++- .../actions/SignUpCognitoActionsTest.kt | 294 +++++++ .../UserAuthSignInCognitoActionsTest.kt | 377 +++++++++ .../WebAuthnSignInCognitoActionsTest.kt | 219 ++++++ .../auth/cognito/featuretest/AuthAPI.kt | 1 + .../generators/SerializationTools.kt | 11 +- .../AuthStateJsonGenerator.kt | 108 ++- .../ConfirmSignInTestCaseGenerator.kt | 359 ++++++++- .../ConfirmSignUpTestCaseGenerator.kt | 257 ++++++ .../SignInTestCaseGenerator.kt | 465 ++++++++++- .../SignUpTestCaseGenerator.kt | 281 ++++++- .../serializers/AuthSignUpResultSerializer.kt | 45 ++ .../serializers/AuthStatesSerializer.kt | 71 +- .../CognitoExceptionSerializers.kt | 12 + .../helpers/SignInChallengeHelperTest.kt | 216 +++-- .../cognito/helpers/WebAuthnHelperTest.kt | 80 ++ .../AssociateWebAuthnCredentialUseCaseTest.kt | 98 +++ .../DeleteWebAuthnCredentialsUseCaseTest.kt | 70 ++ .../ListWebAuthnCredentialsUseCaseTest.kt | 110 +++ .../statemachine/StateMachineTests.kt | 99 ++- .../statemachine/util/MaskUtilTest.kt | 52 ++ .../utilities/AuthOptionsFactory.kt | 14 +- .../utilities/CognitoMockFactory.kt | 22 + .../utilities/CognitoRequestFactory.kt | 30 + .../authconfiguration_userauth.json | 36 + .../states/SignedIn_SessionEstablished.json | 3 + .../states/SignedOut_Configured.json | 3 + ...t_Configured_AwaitingUserConfirmation.json | 21 + ...nEstablished_AwaitingUserConfirmation.json | 45 ++ ...SignedOut_SessionEstablished_SignedUp.json | 46 ++ .../states/SigningIn_EmailOtp.json | 24 + .../states/SigningIn_SelectChallenge.json | 22 + .../states/SigningIn_SigningIn.json | 3 + .../feature-test/states/SigningIn_SmsOtp.json | 24 + ...oper_cognito_request_and_returns_DONE.json | 55 ++ ...utoSignIn_without_ConfirmSignUp_fails.json | 26 + ...orrect_SMS_OTP_code_signs_the_user_in.json | 68 ++ ...rect_email_OTP_code_signs_the_user_in.json | 68 ++ ...ring_the_incorrect_SMS_OTP_code_fails.json | 41 + ...ng_the_incorrect_email_OTP_code_fails.json | 41 + ...ge_with_the_correct_password_succeeds.json | 83 ++ ...nge_with_the_incorrect_password_fails.json | 41 + ...ge_with_the_correct_password_succeeds.json | 47 ++ ...nge_with_the_incorrect_password_fails.json | 41 + ...TP_challenge_returns_the_proper_state.json | 52 ++ ...TP_challenge_returns_the_proper_state.json | 52 ++ ...sswordless_confirmSignUp_returns_Done.json | 47 ++ ...firmSignUp_returns_CompleteAutoSignIn.json | 50 ++ ...d_Confirmation_Code_returns_Exception.json | 52 ++ ...th_Invalid_Username_returns_Exception.json | 52 ++ ...h_Unregistered_User_returns_Exception.json | 52 ++ ...r_cognito_request_and_returns_success.json | 60 ++ ...ence_returns_Confirm_Sign_In_With_OTP.json | 58 ++ ...In_with_PASSWORD_SRP_preference_fails.json | 47 ++ ...with_PASSWORD_SRP_preference_succeeds.json | 89 +++ ...signIn_with_PASSWORD_preference_fails.json | 47 ++ ...nIn_with_PASSWORD_preference_succeeds.json | 53 ++ ...ence_returns_Confirm_Sign_In_With_OTP.json | 58 ++ ...d_preference_returns_Select_Challenge.json | 60 ++ ...o_preference_returns_Select_Challenge.json | 60 ++ ...f_user_is_confirmed_in_the_first_step.json | 6 +- ...d_username_returns_CompleteAutoSignIn.json | 70 ++ ...ignUp_with_an_existing_username_fails.json | 61 ++ ...signUp_with_an_invalid_username_fails.json | 61 ++ ...id_username_returns_ConfirmSignUpStep.json | 69 ++ ...r_cognito_request_and_returns_success.json | 3 +- aws-core/build.gradle.kts | 4 + .../util/DocumentExtensions.kt | 99 +++ .../util/DocumentExtensionsTest.kt | 115 +++ build.gradle.kts | 6 - core-kotlin/api/core-kotlin.api | 11 + .../com/amplifyframework/kotlin/auth/Auth.kt | 62 +- .../kotlin/auth/KotlinAuthFacade.kt | 336 ++++---- .../kotlin/auth/KotlinAuthFacadeTest.kt | 62 ++ core/api/core.api | 91 ++- .../amplifyframework/auth/AuthCategory.java | 69 +- .../auth/AuthCategoryBehavior.java | 94 ++- .../amplifyframework/auth/AuthFactorType.kt | 47 ++ ...AuthAssociateWebAuthnCredentialsOptions.kt | 54 ++ .../AuthDeleteWebAuthnCredentialOptions.kt | 54 ++ .../AuthListWebAuthnCredentialsOptions.kt | 53 ++ .../AuthListWebAuthnCredentialsResult.kt | 53 ++ .../auth/result/step/AuthNextSignInStep.java | 24 +- .../auth/result/step/AuthSignInStep.java | 14 + .../auth/result/step/AuthSignUpStep.java | 7 +- .../amplifyframework/auth/AuthPluginTest.kt | 41 +- gradle/libs.versions.toml | 2 + rxbindings/api/rxbindings.api | 7 + .../amplifyframework/rx/RxAuthBinding.java | 53 +- .../rx/RxAuthCategoryBehavior.java | 72 +- .../rx/RxAuthBindingTest.java | 65 +- settings.gradle.kts | 1 + .../testutils/sync/SynchronousAuth.java | 12 +- 168 files changed, 11539 insertions(+), 1183 deletions(-) create mode 100644 aws-auth-cognito/consumer-rules.pro create mode 100644 aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginUserAuthTests.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/WebAuthnNotEnabledException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnCredentialAlreadyExistsException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnNotSupportedException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnRpMismatchException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFactorTypeHelper.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFlowTypeHelper.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthLogger.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/FlowExtensions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelper.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchAuthSessionUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SignUpActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/UserAuthSignInActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/WebAuthnSignInActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/ChallengeParameter.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignUpData.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/WebAuthnSignInContext.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignUpEvent.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/WebAuthnEvent.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/util/MaskUtil.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/AutoSignInCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActionsTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignUpTestCaseGenerator.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthSignUpResultSerializer.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelperTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/util/MaskUtilTest.kt create mode 100644 aws-auth-cognito/src/test/resources/feature-test/configuration/authconfiguration_userauth.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured_AwaitingUserConfirmation.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_AwaitingUserConfirmation.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_SignedUp.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_EmailOtp.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SelectChallenge.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SmsOtp.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_invokes_proper_cognito_request_and_returns_DONE.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_without_ConfirmSignUp_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_SMS_OTP_code_signs_the_user_in.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_email_OTP_code_signs_the_user_in.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_SMS_OTP_code_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_email_OTP_code_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_correct_password_succeeds.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_incorrect_password_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_correct_password_succeeds.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_incorrect_password_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_SMS_OTP_challenge_returns_the_proper_state.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_email_OTP_challenge_returns_the_proper_state.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_non_passwordless_confirmSignUp_returns_Done.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_returns_CompleteAutoSignIn.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Confirmation_Code_returns_Exception.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Username_returns_Exception.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Unregistered_User_returns_Exception.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_invokes_proper_cognito_request_and_returns_success.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_EMAIL_preference_returns_Confirm_Sign_In_With_OTP.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_succeeds.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_succeeds.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_SMS_preference_returns_Confirm_Sign_In_With_OTP.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_an_unsupported_preference_returns_Select_Challenge.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_no_preference_returns_Select_Challenge.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_confirmed_signUp_with_valid_username_returns_CompleteAutoSignIn.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_existing_username_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_invalid_username_fails.json create mode 100644 aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_uncofirmed_signUp_with_valid_username_returns_ConfirmSignUpStep.json create mode 100644 aws-core/src/main/java/com/amplifyframework/util/DocumentExtensions.kt create mode 100644 aws-core/src/test/java/com/amplifyframework/util/DocumentExtensionsTest.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/AuthFactorType.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult.kt diff --git a/aws-auth-cognito/api/aws-auth-cognito.api b/aws-auth-cognito/api/aws-auth-cognito.api index af4b955052..3582e4ef7b 100644 --- a/aws-auth-cognito/api/aws-auth-cognito.api +++ b/aws-auth-cognito/api/aws-auth-cognito.api @@ -10,6 +10,9 @@ public final class com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin : com/ public static final field AWS_COGNITO_AUTH_LOG_NAMESPACE Ljava/lang/String; public static final field Companion Lcom/amplifyframework/auth/cognito/AWSCognitoAuthPlugin$Companion; public fun ()V + public fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun autoSignIn (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public final fun clearFederationToIdentityPool (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun configure (Lorg/json/JSONObject;Landroid/content/Context;)V public fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V @@ -20,6 +23,8 @@ public final class com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin : com/ public fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun deleteUser (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public final fun federateToIdentityPool (Ljava/lang/String;Lcom/amplifyframework/auth/AuthProvider;Lcom/amplifyframework/auth/cognito/options/FederateToIdentityPoolOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public final fun federateToIdentityPool (Ljava/lang/String;Lcom/amplifyframework/auth/AuthProvider;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -36,6 +41,8 @@ public final class com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin : com/ public fun getVersion ()Ljava/lang/String; public fun handleWebUISignInResponse (Landroid/content/Intent;)V public fun initialize (Landroid/content/Context;)V + public fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V + public fun listWebAuthnCredentials (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun rememberDevice (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -238,7 +245,8 @@ public class com/amplifyframework/auth/cognito/exceptions/service/TooManyRequest } public class com/amplifyframework/auth/cognito/exceptions/service/UserCancelledException : com/amplifyframework/auth/exceptions/ServiceException { - public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } public class com/amplifyframework/auth/cognito/exceptions/service/UserLambdaValidationException : com/amplifyframework/auth/exceptions/ServiceException { @@ -257,6 +265,21 @@ public class com/amplifyframework/auth/cognito/exceptions/service/UsernameExists public fun (Ljava/lang/Throwable;)V } +public final class com/amplifyframework/auth/cognito/exceptions/service/WebAuthnNotEnabledException : com/amplifyframework/auth/exceptions/ServiceException { +} + +public final class com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnCredentialAlreadyExistsException : com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException { +} + +public class com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException : com/amplifyframework/auth/AuthException { +} + +public final class com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnNotSupportedException : com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException { +} + +public final class com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnRpMismatchException : com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException { +} + public final class com/amplifyframework/auth/cognito/helpers/FlutterFactory { public static final field INSTANCE Lcom/amplifyframework/auth/cognito/helpers/FlutterFactory; public final fun createAWSCognitoAuthSession (ZLcom/amplifyframework/auth/result/AuthSessionResult;Lcom/amplifyframework/auth/result/AuthSessionResult;Lcom/amplifyframework/auth/result/AuthSessionResult;Lcom/amplifyframework/auth/result/AuthSessionResult;)Lcom/amplifyframework/auth/cognito/AWSCognitoAuthSession; @@ -295,9 +318,11 @@ public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfi public final fun component1 ()Ljava/util/Map; public final fun component2 ()Ljava/util/List; public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/util/Map;Ljava/util/List;Ljava/lang/String;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions; - public static synthetic fun copy$default (Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions; + public final fun component4 ()Ljava/lang/ref/WeakReference; + public final fun copy (Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/ref/WeakReference;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions; + public static synthetic fun copy$default (Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/ref/WeakReference;ILjava/lang/Object;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions; public fun equals (Ljava/lang/Object;)Z + public final fun getCallingActivity ()Ljava/lang/ref/WeakReference; public final fun getFriendlyDeviceName ()Ljava/lang/String; public final fun getMetadata ()Ljava/util/Map; public final fun getUserAttributes ()Ljava/util/List; @@ -309,6 +334,7 @@ public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfi public fun ()V public fun build ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions; public synthetic fun build ()Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions; + public final fun callingActivity (Landroid/app/Activity;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions$CognitoBuilder; public final fun friendlyDeviceName (Ljava/lang/String;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions$CognitoBuilder; public fun getThis ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions$CognitoBuilder; public synthetic fun getThis ()Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions$Builder; @@ -347,6 +373,41 @@ public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfi public final fun invoke (Lkotlin/jvm/functions/Function1;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignUpOptions; } +public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions : com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions { + public static final field Companion Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Companion; + public static final fun builder ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder; + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; + public static synthetic fun copy$default (Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; + public static final fun defaults ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getMaxResults ()Ljava/lang/Integer; + public final fun getNextToken ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder : com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Builder { + public fun ()V + public fun build ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; + public synthetic fun build ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions; + public final fun getMaxResults ()Ljava/lang/Integer; + public final fun getNextToken ()Ljava/lang/String; + public fun getThis ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder; + public synthetic fun getThis ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Builder; + public final fun maxResults (Ljava/lang/Integer;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder; + public final fun nextToken (Ljava/lang/String;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder; + public final synthetic fun setMaxResults (Ljava/lang/Integer;)V + public final synthetic fun setNextToken (Ljava/lang/String;)V +} + +public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Companion { + public final fun builder ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions$Builder; + public final fun defaults ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; + public final synthetic fun invoke (Lkotlin/jvm/functions/Function1;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions; +} + public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthResendSignUpCodeOptions : com/amplifyframework/auth/options/AuthResendSignUpCodeOptions { public static final field Companion Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthResendSignUpCodeOptions$Companion; public static final fun builder ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthResendSignUpCodeOptions$CognitoBuilder; @@ -429,7 +490,9 @@ public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignI public static fun builder ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; public fun equals (Ljava/lang/Object;)Z public fun getAuthFlowType ()Lcom/amplifyframework/auth/cognito/options/AuthFlowType; + public fun getCallingActivity ()Ljava/lang/ref/WeakReference; public fun getMetadata ()Ljava/util/Map; + public fun getPreferredFirstFactor ()Lcom/amplifyframework/auth/AuthFactorType; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -439,9 +502,11 @@ public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignI public fun authFlowType (Lcom/amplifyframework/auth/cognito/options/AuthFlowType;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; public fun build ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions; public synthetic fun build ()Lcom/amplifyframework/auth/options/AuthSignInOptions; + public fun callingActivity (Landroid/app/Activity;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; public fun getThis ()Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; public synthetic fun getThis ()Lcom/amplifyframework/auth/options/AuthSignInOptions$Builder; public fun metadata (Ljava/util/Map;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; + public fun preferredFirstFactor (Lcom/amplifyframework/auth/AuthFactorType;)Lcom/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions$CognitoBuilder; } public final class com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignOutOptions : com/amplifyframework/auth/options/AuthSignOutOptions { @@ -586,6 +651,7 @@ public final class com/amplifyframework/auth/cognito/options/AuthFlowType : java public static final field CUSTOM_AUTH Lcom/amplifyframework/auth/cognito/options/AuthFlowType; public static final field CUSTOM_AUTH_WITHOUT_SRP Lcom/amplifyframework/auth/cognito/options/AuthFlowType; public static final field CUSTOM_AUTH_WITH_SRP Lcom/amplifyframework/auth/cognito/options/AuthFlowType; + public static final field USER_AUTH Lcom/amplifyframework/auth/cognito/options/AuthFlowType; public static final field USER_PASSWORD_AUTH Lcom/amplifyframework/auth/cognito/options/AuthFlowType; public static final field USER_SRP_AUTH Lcom/amplifyframework/auth/cognito/options/AuthFlowType; public static fun valueOf (Ljava/lang/String;)Lcom/amplifyframework/auth/cognito/options/AuthFlowType; @@ -615,6 +681,19 @@ public final class com/amplifyframework/auth/cognito/options/FederateToIdentityP public final fun invoke (Lkotlin/jvm/functions/Function1;)Lcom/amplifyframework/auth/cognito/options/FederateToIdentityPoolOptions; } +public final class com/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult : com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult { + public fun (Ljava/util/List;Ljava/lang/String;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/lang/String;)Lcom/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult; + public static synthetic fun copy$default (Lcom/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcom/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult; + public fun equals (Ljava/lang/Object;)Z + public fun getCredentials ()Ljava/util/List; + public final fun getNextToken ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract class com/amplifyframework/auth/cognito/result/AWSCognitoAuthSignOutResult : com/amplifyframework/auth/result/AuthSignOutResult { public abstract fun getSignedOutLocally ()Z } diff --git a/aws-auth-cognito/build.gradle.kts b/aws-auth-cognito/build.gradle.kts index 93585fe2e9..3731c89ae5 100644 --- a/aws-auth-cognito/build.gradle.kts +++ b/aws-auth-cognito/build.gradle.kts @@ -26,6 +26,9 @@ group = properties["POM_GROUP"].toString() android { namespace = "com.amplifyframework.auth.cognito" + defaultConfig { + consumerProguardFiles += file("consumer-rules.pro") + } } dependencies { @@ -37,6 +40,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.security) implementation(libs.androidx.browser) + implementation(libs.androidx.credentials) implementation(libs.aws.http) implementation(libs.aws.cognitoidentity) @@ -61,6 +65,7 @@ dependencies { testImplementation(libs.test.kotlin.reflection) testImplementation(libs.test.kotest.assertions) testImplementation(libs.test.kotest.assertions.json) + testImplementation(libs.test.turbine) androidTestImplementation(libs.gson) //noinspection GradleDependency diff --git a/aws-auth-cognito/consumer-rules.pro b/aws-auth-cognito/consumer-rules.pro new file mode 100644 index 0000000000..c86fde59d8 --- /dev/null +++ b/aws-auth-cognito/consumer-rules.pro @@ -0,0 +1,5 @@ +# CredentialManager rules +-if class androidx.credentials.CredentialManager +-keep class androidx.credentials.playservices.** { + *; +} \ No newline at end of file diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt index 026d45ffd7..23a7bb2302 100644 --- a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt @@ -18,6 +18,7 @@ package com.amplifyframework.auth.cognito import android.content.Context import androidx.test.core.app.ApplicationProvider import com.amplifyframework.api.aws.AWSApiPlugin +import com.amplifyframework.api.graphql.GraphQLOperation import com.amplifyframework.api.graphql.SimpleGraphQLRequest import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey @@ -51,6 +52,7 @@ class AWSCognitoAuthPluginEmailMFATests { private var authPlugin = AWSCognitoAuthPlugin() private var apiPlugin = AWSApiPlugin() private lateinit var synchronousAuth: SynchronousAuth + private var subscription: GraphQLOperation? = null private var mfaCode = "" private var latch: CountDownLatch? = null @@ -64,8 +66,8 @@ class AWSCognitoAuthPluginEmailMFATests { apiPlugin.configure(config, context) synchronousAuth = SynchronousAuth.delegatingTo(authPlugin) - apiPlugin.subscribe( - SimpleGraphQLRequest( + subscription = apiPlugin.subscribe( + SimpleGraphQLRequest( Assets.readAsString("create-mfa-subscription.graphql"), MfaInfo::class.java, null @@ -83,6 +85,7 @@ class AWSCognitoAuthPluginEmailMFATests { @After fun tearDown() { + subscription?.cancel() mfaCode = "" synchronousAuth.deleteUser() } diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginUserAuthTests.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginUserAuthTests.kt new file mode 100644 index 0000000000..6981439c9c --- /dev/null +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginUserAuthTests.kt @@ -0,0 +1,668 @@ +/* + * Copyright 2024 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 + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.AWSApiPlugin +import com.amplifyframework.api.graphql.GraphQLOperation +import com.amplifyframework.api.graphql.SimpleGraphQLRequest +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.AuthUserAttribute +import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.cognito.exceptions.service.CodeMismatchException +import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException +import com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException +import com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.auth.cognito.test.R +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.NotAuthorizedException +import com.amplifyframework.auth.exceptions.SignedOutException +import com.amplifyframework.auth.options.AuthSignUpOptions +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.auth.result.step.AuthSignUpStep +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.category.CategoryConfiguration +import com.amplifyframework.core.category.CategoryType +import com.amplifyframework.datastore.generated.model.MfaInfo +import com.amplifyframework.testutils.Assets +import com.amplifyframework.testutils.sync.SynchronousAuth +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import org.junit.After +import org.junit.Before +import org.junit.Test + +class AWSCognitoAuthPluginUserAuthTests { + + private val password = "${UUID.randomUUID()}BleepBloop1234!" + private val userName = "test${Random.nextInt()}" + private val email = "$userName@amplify-swift-gamma.awsapps.com" + private val phoneNumber = "+1555${Random.nextInt(1000000, 10000000)}" + + private var authPlugin = AWSCognitoAuthPlugin() + private var apiPlugin = AWSApiPlugin() + private lateinit var synchronousAuth: SynchronousAuth + private var subscription: GraphQLOperation? = null + private var otpCode = "" + private var latch: CountDownLatch? = null + + @Before + fun initializePlugin() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfiguration_passwordless) + val authConfig: CategoryConfiguration = config.forCategoryType(CategoryType.AUTH) + val authConfigJson = authConfig.getPluginConfig("awsCognitoAuthPlugin") + + val apiConfig: CategoryConfiguration = config.forCategoryType(CategoryType.API) + val apiConfigJson = apiConfig.getPluginConfig("awsAPIPlugin") + + authPlugin.configure(authConfigJson, context) + apiPlugin.configure(apiConfigJson, context) + synchronousAuth = SynchronousAuth.delegatingTo(authPlugin) + + subscription = apiPlugin.subscribe( + SimpleGraphQLRequest( + Assets.readAsString("create-mfa-subscription.graphql"), + MfaInfo::class.java, + null + ), + { println("====== Subscription Established ======") }, + { + println("====== Received some MFA Info ======") + otpCode = it.data.code + latch?.countDown() + }, + { println("====== Subscription Failed $it ======") }, + { } + ) + } + + @After + fun tearDown() { + subscription?.cancel() + otpCode = "" + try { + synchronousAuth.deleteUser() + } catch (e: SignedOutException) { + // Catching this because if an assert fails and the test can't delete a user (because the test failed + // before the sign in fully finishes) then deleteUser will always throw an exception because there isn't a + // signed in user and that exception will hide the previous, more relevant, exception + println("Encountered an exception trying to delete the user: $e") + } + } + + @Test + fun signInWithAnIncompatiblePreferredFirstFactorShowsSelectChallenge() { + // Step 1: Sign up a new user with NO phone number and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = false + ) + + // Step 2: Attempt to sign in with the newly created user with SMS as the preferred first factor + // (Inherently incompatible since the account has no phone number associated with it) + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.SMS_OTP) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor (since SMS isn't a proper option) + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a proper first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.EMAIL_OTP.name) + + // Validation 2: Validate that the next step is to confirm the emailed OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the emailed OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 4: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + // Email related tests + + @Test + fun signInWithNoFirstFactorPreferenceAndSelectEmailSucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true, + usePassword = true + ) + + // Step 2: Attempt to sign in with the newly created user with NO preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(null) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.EMAIL_OTP.name) + + // Validation 2: Validate that the next step is to confirm the emailed OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the emailed OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 4: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithNoFirstFactorPreferenceAndSelectEmailRetrySucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true + ) + + // Step 2: Attempt to sign in with the newly created user with NO preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(null) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.EMAIL_OTP.name) + + // Validation 2: Validate that the next step is to confirm the emailed OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Validation 3: Validate that providing an incorrect OTP code throws the proper exception + assertFailsWith { + synchronousAuth.confirmSignIn(otpCode.reversed()) + } + + // Step 4: Input the emailed OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 4: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithEmailPreferredSucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser() + + // Step 2: Attempt to sign in with the newly created user with EMAIL preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.EMAIL_OTP) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to confirm the emailed OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 3: Input the emailed OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 2: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithEmailPreferredRetrySucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser() + + // Step 2: Attempt to sign in with the newly created user with EMAIL preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.EMAIL_OTP) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to confirm the emailed OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Validation 2: Validate that providing an incorrect OTP code throws the proper exception + assertFailsWith { + synchronousAuth.confirmSignIn(otpCode.reversed()) + } + + // Step 3: Input the emailed OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 3: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + // SMS Related Tests + + @Test + fun signInWithNoFirstFactorPreferenceAndSelectSmsSucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true + ) + + // Step 2: Attempt to sign in with the newly created user with NO preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(null) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.SMS_OTP.name) + + // Validation 2: Validate that the next step is to confirm the texted OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the texted OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 3: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithNoFirstFactorPreferenceAndSelectSmsRetrySucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true + ) + + // Step 2: Attempt to sign in with the newly created user with NO preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(null) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.SMS_OTP.name) + + // Validation 2: Validate that the next step is to confirm the texted OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Validation 3: Validate that providing an incorrect OTP code throws the proper exception + assertFailsWith { + synchronousAuth.confirmSignIn(otpCode.reversed()) + } + + // Step 4: Input the texted OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 4: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithSmsPreferredSucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true + ) + + // Step 2: Attempt to sign in with the newly created user with SMS preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.SMS_OTP) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to confirm the texted OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the texted OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 2: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithSmsPreferredRetrySucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePhoneNumber = true + ) + + // Step 2: Attempt to sign in with the newly created user with SMS preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.SMS_OTP) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to confirm the texted OTP code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, signInResult.nextStep.signInStep) + + // Wait until the OTP code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Validation 2: Validate that providing an incorrect OTP code throws the proper exception + assertFailsWith { + synchronousAuth.confirmSignIn(otpCode.reversed()) + } + + // Step 3: Input the texted OTP code for confirmation + signInResult = synchronousAuth.confirmSignIn(otpCode) + + // Validation 2: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + // Password Related Tests + + @Test + fun signInWithNoFirstFactorPreferenceAndSelectPasswordSucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePassword = true + ) + + // Step 2: Attempt to sign in with the newly created user with NO preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(null) + .build() + + var signInResult = synchronousAuth.signIn(userName, null, options) + + // Validation 1: Validate that the next step is to select a first factor + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, signInResult.nextStep.signInStep) + + // Step 3: Select a first factor option + signInResult = synchronousAuth.confirmSignIn(AuthFactorType.PASSWORD.name) + + // Validation 2: Validate that the user needs to input their password + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD, signInResult.nextStep.signInStep) + + // Step 4: Input the password + signInResult = synchronousAuth.confirmSignIn(password) + + // Validation 3: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun signInWithPasswordPreferredRetrySucceeds() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePassword = true + ) + + // Step 2: Attempt to sign in with the newly created user with PASSWORD preferred first factor + val options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD) + .build() + + // Validation 1: Validate that an incorrect password returns the proper exception + assertFailsWith { + synchronousAuth.signIn(userName, password.reversed(), options) + } + + // Step 3: Sign in with the correct password + val signInResult = synchronousAuth.signIn(userName, password, options) + + // Validation 2: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + // Sign Up + + @Test + fun signUpWithEmptyUsernameFails() { + // Step 1: Try to sign up a new user with an invalid/empty username + val options = AuthSignUpOptions.builder() + .userAttributes(listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email))) + .build() + + // Validation 1: Validate that sign up fails because a username is required + assertFailsWith { + synchronousAuth.signUp("", null, options) + } + } + + @Test + fun signUpWithSameUsernameFails() { + // Step 1: Sign up a new user and confirm it + signUpAndConfirmNewUser( + usePassword = true + ) + + // Step 2: Try to sign up a user with the same username + val signUpOptions = AuthSignUpOptions.builder() + .userAttributes(listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email))) + .build() + + // Validation 1: Validate that sign up fails because the username was already taken + assertFailsWith { + synchronousAuth.signUp(userName, password, signUpOptions) + } + + // Step 3: Sign in so that we can delete the user in the tear down + val signInOptions = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD) + .build() + + // Step 3: Sign in with the correct password + val signInResult = synchronousAuth.signIn(userName, password, signInOptions) + + // Validation 2: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + // Confirm Sign Up and Auto Sign In + + @Test + fun confirmSignUpAndAutoSignInSucceeds() { + // Step 1: Sign up a passwordless user with just an email address + val options = AuthSignUpOptions.builder() + .userAttributes(listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email))) + .build() + var signUpResult = synchronousAuth.signUp(userName, null, options) + + // Validation 1: Validate that the user is currently in the Confirm Sign Up state + assertEquals(AuthSignUpStep.CONFIRM_SIGN_UP_STEP, signUpResult.nextStep.signUpStep) + + // Validation 2: Validate that calling auto sign in before the user is confirmed fails + assertFailsWith { + synchronousAuth.autoSignIn() + } + + // Wait until the confirmation code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Step 2: Confirm sign up with the correct OTP code + signUpResult = synchronousAuth.confirmSignUp(userName, otpCode) + + // Validation 3: Validate that the user confirmation is complete and that auto sign in can be completed + assertEquals(AuthSignUpStep.COMPLETE_AUTO_SIGN_IN, signUpResult.nextStep.signUpStep) + + // Step 3: Sign in so that we can delete the user in the tear down + val signInResult = synchronousAuth.autoSignIn() + + // Validation 4: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun confirmSignUpRetryAndAutoSignInSucceeds() { + // Step 1: Sign up a passwordless user with just an email address + val options = AuthSignUpOptions.builder() + .userAttributes(listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email))) + .build() + var signUpResult = synchronousAuth.signUp(userName, null, options) + + // Validation 1: Validate that the user is currently in the Confirm Sign Up state + assertEquals(AuthSignUpStep.CONFIRM_SIGN_UP_STEP, signUpResult.nextStep.signUpStep) + + // Validation 2: Validate that calling auto sign in before the user is confirmed fails + assertFailsWith { + synchronousAuth.autoSignIn() + } + + // Wait until the confirmation code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + // Validation 3: Validate that confirm sign up fails the OTP code is incorrect + assertFailsWith { + synchronousAuth.confirmSignUp(userName, otpCode.reversed()) + } + + // Step 2: Confirm sign up with the correct OTP code + signUpResult = synchronousAuth.confirmSignUp(userName, otpCode) + + // Validation 4: Validate that the user confirmation is complete and that auto sign in can be completed + assertEquals(AuthSignUpStep.COMPLETE_AUTO_SIGN_IN, signUpResult.nextStep.signUpStep) + + // Step 3: Sign in so that we can delete the user in the tear down + val signInResult = synchronousAuth.autoSignIn() + + // Validation 5: Validate that user is signed in + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun confirmSignUpFailsForUnregisteredUser() { + // Validation 1: Validate that confirm sign up fails on an unregistered user + assertFailsWith { + synchronousAuth.confirmSignUp(userName, "123456") + } + } + + @Test + fun confirmSignUpFailsForEmptyUser() { + // Validation 1: Validate that confirm sign up fails on an invalid username + assertFailsWith { + synchronousAuth.confirmSignUp("", "123456") + } + } + + private fun signUpAndConfirmNewUser(usePhoneNumber: Boolean = false, usePassword: Boolean = false) { + val signUpPassword = if (usePassword) { + password + } else { + null + } + val attributes = if (usePhoneNumber) { + listOf( + AuthUserAttribute(AuthUserAttributeKey.email(), email), + AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), phoneNumber) + ) + } else { + listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email)) + } + + val options = AuthSignUpOptions.builder() + .userAttributes(attributes).build() + synchronousAuth.signUp(userName, signUpPassword, options) + + // Wait until the confirmation code has been received + latch = CountDownLatch(1) + latch?.await(20, TimeUnit.SECONDS) + + synchronousAuth.confirmSignUp(userName, otpCode) + } +} 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 4771fa5403..5340c90fa6 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 @@ -33,15 +33,19 @@ import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.asf.UserContextDataProvider +import com.amplifyframework.auth.cognito.helpers.authLogger import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult +import com.amplifyframework.auth.cognito.usecases.AuthUseCaseFactory import com.amplifyframework.auth.exceptions.ConfigurationException -import com.amplifyframework.auth.exceptions.UnknownException +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.options.AuthResetPasswordOptions @@ -52,15 +56,14 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignOutResult import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.AuthUpdateAttributeResult import com.amplifyframework.core.Action -import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer -import com.amplifyframework.core.category.CategoryType import com.amplifyframework.core.configuration.AmplifyOutputsData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -80,12 +83,14 @@ class AWSCognitoAuthPlugin : AuthPlugin() { private const val AWS_COGNITO_AUTH_PLUGIN_KEY = "awsCognitoAuthPlugin" } - private val logger = - Amplify.Logging.logger(CategoryType.AUTH, AWS_COGNITO_AUTH_LOG_NAMESPACE.format(this::class.java.simpleName)) + private val logger = authLogger() @VisibleForTesting internal lateinit var realPlugin: RealAWSCognitoAuthPlugin + @VisibleForTesting + internal lateinit var useCaseFactory: AuthUseCaseFactory + private val pluginScope = CoroutineScope(Job() + Dispatchers.Default) private val queueFacade: KotlinAuthFacadeInternal by lazy { KotlinAuthFacadeInternal(realPlugin) @@ -115,7 +120,10 @@ class AWSCognitoAuthPlugin : AuthPlugin() { private fun Exception.toAuthException(): AuthException = if (this is AuthException) { this } else { - UnknownException(cause = this) + CognitoAuthExceptionConverter.lookup( + error = this, + fallbackMessage = "An unclassified error prevented this operation." + ) } override fun initialize(context: Context) { @@ -168,6 +176,8 @@ class AWSCognitoAuthPlugin : AuthPlugin() { logger ) + useCaseFactory = AuthUseCaseFactory(realPlugin, authEnvironment, authStateMachine) + blockQueueChannelWhileConfiguring() } @@ -183,7 +193,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { override fun signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions, onSuccess: Consumer, onError: Consumer @@ -417,6 +427,48 @@ class AWSCognitoAuthPlugin : AuthPlugin() { onError: Consumer ) = enqueue(onSuccess, onError) { queueFacade.verifyTOTPSetup(code, options) } + override fun associateWebAuthnCredential( + callingActivity: Activity, + onSuccess: Action, + onError: Consumer + ) = associateWebAuthnCredential( + callingActivity, + AuthAssociateWebAuthnCredentialsOptions.defaults(), + onSuccess, + onError + ) + + override fun associateWebAuthnCredential( + callingActivity: Activity, + options: AuthAssociateWebAuthnCredentialsOptions, + onSuccess: Action, + onError: Consumer + ) = enqueue(onSuccess, onError) { useCaseFactory.associateWebAuthnCredential().execute(callingActivity, options) } + + override fun listWebAuthnCredentials( + onSuccess: Consumer, + onError: Consumer + ) = listWebAuthnCredentials(AuthListWebAuthnCredentialsOptions.defaults(), onSuccess, onError) + + override fun listWebAuthnCredentials( + options: AuthListWebAuthnCredentialsOptions, + onSuccess: Consumer, + onError: Consumer + ) = enqueue(onSuccess, onError) { useCaseFactory.listWebAuthnCredentials().execute(options) } + + override fun autoSignIn(onSuccess: Consumer, onError: Consumer) = + enqueue(onSuccess, onError) { queueFacade.autoSignIn() } + + override fun deleteWebAuthnCredential(credentialId: String, onSuccess: Action, onError: Consumer) = + deleteWebAuthnCredential(credentialId, AuthDeleteWebAuthnCredentialOptions.defaults(), onSuccess, onError) + + override fun deleteWebAuthnCredential( + credentialId: String, + options: AuthDeleteWebAuthnCredentialOptions, + onSuccess: Action, + onError: Consumer + ) = enqueue(onSuccess, onError) { useCaseFactory.deleteWebAuthnCredential().execute(credentialId, options) } + override fun getEscapeHatch() = realPlugin.escapeHatch() override fun getPluginKey() = AWS_COGNITO_AUTH_PLUGIN_KEY diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthEnvironment.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthEnvironment.kt index f38dc12f73..c7b5e75be5 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthEnvironment.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthEnvironment.kt @@ -19,6 +19,7 @@ import android.annotation.SuppressLint import android.content.Context import com.amplifyframework.auth.cognito.asf.UserContextDataProvider import com.amplifyframework.auth.cognito.helpers.SRPHelper +import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.Environment import com.amplifyframework.statemachine.StateMachineEvent @@ -30,6 +31,7 @@ import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent import com.amplifyframework.statemachine.codegen.events.SignOutEvent +import com.amplifyframework.statemachine.codegen.events.SignUpEvent import java.util.Date import java.util.UUID @@ -96,7 +98,9 @@ internal class AuthEnvironment internal constructor( val newASFDevice = AmplifyCredential.ASFDevice(newDeviceId) credentialStoreClient.storeCredentials(CredentialType.ASF, newASFDevice) newDeviceId - } else asfDevice.id + } else { + asfDevice.id + } return userContextDataProvider?.getEncodedContextData(username, deviceId) } @@ -112,22 +116,19 @@ internal class AuthEnvironment internal constructor( } } -internal fun StateMachineEvent.isAuthEvent(): AuthEvent.EventType? { - return (this as? AuthEvent)?.eventType -} +internal fun AuthEnvironment.requireIdentityProviderClient() = cognitoAuthService.cognitoIdentityProviderClient + ?: throw InvalidStateException("No Cognito identity provider client available") -internal fun StateMachineEvent.isAuthenticationEvent(): AuthenticationEvent.EventType? { - return (this as? AuthenticationEvent)?.eventType -} +internal fun StateMachineEvent.isAuthEvent(): AuthEvent.EventType? = (this as? AuthEvent)?.eventType -internal fun StateMachineEvent.isAuthorizationEvent(): AuthorizationEvent.EventType? { - return (this as? AuthorizationEvent)?.eventType -} +internal fun StateMachineEvent.isAuthenticationEvent(): AuthenticationEvent.EventType? = + (this as? AuthenticationEvent)?.eventType -internal fun StateMachineEvent.isSignOutEvent(): SignOutEvent.EventType? { - return (this as? SignOutEvent)?.eventType -} +internal fun StateMachineEvent.isAuthorizationEvent(): AuthorizationEvent.EventType? = + (this as? AuthorizationEvent)?.eventType -internal fun StateMachineEvent.isDeleteUserEvent(): DeleteUserEvent.EventType? { - return (this as? DeleteUserEvent)?.eventType -} +internal fun StateMachineEvent.isSignOutEvent(): SignOutEvent.EventType? = (this as? SignOutEvent)?.eventType + +internal fun StateMachineEvent.isDeleteUserEvent(): DeleteUserEvent.EventType? = (this as? DeleteUserEvent)?.eventType + +internal fun StateMachineEvent.isSignUpEvent(): SignUpEvent.EventType? = (this as? SignUpEvent)?.eventType 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 a781329481..66d4f081c4 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 @@ -29,6 +29,10 @@ import com.amplifyframework.auth.cognito.actions.SignInChallengeCognitoActions import com.amplifyframework.auth.cognito.actions.SignInCognitoActions import com.amplifyframework.auth.cognito.actions.SignInCustomCognitoActions import com.amplifyframework.auth.cognito.actions.SignOutCognitoActions +import com.amplifyframework.auth.cognito.actions.SignUpCognitoActions +import com.amplifyframework.auth.cognito.actions.UserAuthSignInCognitoActions +import com.amplifyframework.auth.cognito.actions.WebAuthnSignInCognitoActions +import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.statemachine.Environment import com.amplifyframework.statemachine.StateMachine import com.amplifyframework.statemachine.StateMachineResolver @@ -47,13 +51,14 @@ 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.SignUpState +import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState internal class AuthStateMachine( resolver: StateMachineResolver, environment: Environment, initialState: AuthState? = null -) : - StateMachine(resolver, environment, initialState = initialState) { +) : StateMachine(resolver, environment, initialState = initialState) { constructor(environment: Environment, initialState: AuthState? = null) : this( AuthState.Resolver( AuthenticationState.Resolver( @@ -65,6 +70,8 @@ internal class AuthStateMachine( HostedUISignInState.Resolver(HostedUICognitoActions), DeviceSRPSignInState.Resolver(DeviceSRPCognitoSignInActions), SetupTOTPState.Resolver(SetupTOTPCognitoActions), + WebAuthnSignInState.Resolver(WebAuthnSignInCognitoActions, SignInCognitoActions), + UserAuthSignInCognitoActions, SignInCognitoActions ), SignOutState.Resolver(SignOutCognitoActions), @@ -79,7 +86,8 @@ internal class AuthStateMachine( DeleteUserState.Resolver(DeleteUserCognitoActions), AuthorizationCognitoActions ), - AuthCognitoActions + AuthCognitoActions, + SignUpState.Resolver(SignUpCognitoActions) ), environment, initialState @@ -97,6 +105,8 @@ internal class AuthStateMachine( HostedUISignInState.Resolver(HostedUICognitoActions).logging(), DeviceSRPSignInState.Resolver(DeviceSRPCognitoSignInActions).logging(), SetupTOTPState.Resolver(SetupTOTPCognitoActions).logging(), + WebAuthnSignInState.Resolver(WebAuthnSignInCognitoActions, SignInCognitoActions).logging(), + UserAuthSignInCognitoActions, SignInCognitoActions ).logging(), SignOutState.Resolver(SignOutCognitoActions).logging(), @@ -111,9 +121,19 @@ internal class AuthStateMachine( DeleteUserState.Resolver(DeleteUserCognitoActions), AuthorizationCognitoActions ).logging(), - AuthCognitoActions + AuthCognitoActions, + SignUpState.Resolver(SignUpCognitoActions).logging() ).logging(), environment ) } } + +// 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}" + ) + } +} 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 2081ffc563..7accf127e3 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 @@ -34,6 +34,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserLambdaValidatio import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotConfirmedException import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundException import aws.sdk.kotlin.services.cognitoidentityprovider.model.UsernameExistsException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.WebAuthnNotEnabledException import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.cognito.exceptions.service.CodeExpiredException import com.amplifyframework.auth.cognito.exceptions.service.FailedAttemptsLimitExceededException @@ -54,51 +55,54 @@ internal class CognitoAuthExceptionConverter { * @param fallbackMessage Fallback message to inform failure * @return AuthException Specific exception for Amplify Auth */ - fun lookup(error: Exception, fallbackMessage: String): AuthException { - return when (error) { - is UserNotFoundException -> com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException( + fun lookup(error: Exception, fallbackMessage: String): AuthException = when (error) { + is AuthException -> error + is UserNotFoundException -> com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException( + error + ) + is UserNotConfirmedException -> + com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException(error) + is UsernameExistsException -> + com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException(error) + is AliasExistsException -> com.amplifyframework.auth.cognito.exceptions.service.AliasExistsException( + error + ) + is InvalidPasswordException -> + com.amplifyframework.auth.cognito.exceptions.service.InvalidPasswordException(error) + is InvalidParameterException -> + com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException(cause = error) + is ExpiredCodeException -> CodeExpiredException(error) + is CodeMismatchException -> com.amplifyframework.auth.cognito.exceptions.service.CodeMismatchException( + error + ) + is CodeDeliveryFailureException -> + com.amplifyframework.auth.cognito.exceptions.service.CodeDeliveryFailureException(error) + is LimitExceededException -> + com.amplifyframework.auth.cognito.exceptions.service.LimitExceededException(error) + is MfaMethodNotFoundException -> MFAMethodNotFoundException(error) + is NotAuthorizedException -> com.amplifyframework.auth.exceptions.NotAuthorizedException(cause = error) + is ResourceNotFoundException -> + com.amplifyframework.auth.cognito.exceptions.service.ResourceNotFoundException(error) + is SoftwareTokenMfaNotFoundException -> + SoftwareTokenMFANotFoundException(error) + is TooManyFailedAttemptsException -> + FailedAttemptsLimitExceededException(error) + is TooManyRequestsException -> + com.amplifyframework.auth.cognito.exceptions.service.TooManyRequestsException(error) + is PasswordResetRequiredException -> + com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException(error) + is EnableSoftwareTokenMfaException -> + com.amplifyframework.auth.cognito.exceptions.service.EnableSoftwareTokenMFAException(error) + is UserLambdaValidationException -> + com.amplifyframework.auth.cognito.exceptions.service.UserLambdaValidationException( + error.message, error ) - is UserNotConfirmedException -> - com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException(error) - is UsernameExistsException -> - com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException(error) - is AliasExistsException -> com.amplifyframework.auth.cognito.exceptions.service.AliasExistsException( - error - ) - is InvalidPasswordException -> - com.amplifyframework.auth.cognito.exceptions.service.InvalidPasswordException(error) - is InvalidParameterException -> - com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException(cause = error) - is ExpiredCodeException -> CodeExpiredException(error) - is CodeMismatchException -> com.amplifyframework.auth.cognito.exceptions.service.CodeMismatchException( - error + is WebAuthnNotEnabledException -> + com.amplifyframework.auth.cognito.exceptions.service.WebAuthnNotEnabledException( + cause = error ) - is CodeDeliveryFailureException -> - com.amplifyframework.auth.cognito.exceptions.service.CodeDeliveryFailureException(error) - is LimitExceededException -> - com.amplifyframework.auth.cognito.exceptions.service.LimitExceededException(error) - is MfaMethodNotFoundException -> MFAMethodNotFoundException(error) - is NotAuthorizedException -> com.amplifyframework.auth.exceptions.NotAuthorizedException(cause = error) - is ResourceNotFoundException -> - com.amplifyframework.auth.cognito.exceptions.service.ResourceNotFoundException(error) - is SoftwareTokenMfaNotFoundException -> - SoftwareTokenMFANotFoundException(error) - is TooManyFailedAttemptsException -> - FailedAttemptsLimitExceededException(error) - is TooManyRequestsException -> - com.amplifyframework.auth.cognito.exceptions.service.TooManyRequestsException(error) - is PasswordResetRequiredException -> - com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException(error) - is EnableSoftwareTokenMfaException -> - com.amplifyframework.auth.cognito.exceptions.service.EnableSoftwareTokenMFAException(error) - is UserLambdaValidationException -> - com.amplifyframework.auth.cognito.exceptions.service.UserLambdaValidationException( - error.message, - error - ) - else -> UnknownException(fallbackMessage, error) - } + else -> UnknownException(fallbackMessage, error) } } } 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 ae448ac3aa..76e63a33d0 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 @@ -54,7 +54,7 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth suspend fun signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions ): AuthSignUpResult { return suspendCoroutine { continuation -> @@ -566,4 +566,13 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth ) } } + + suspend fun autoSignIn(): AuthSignInResult { + return suspendCoroutine { continuation -> + delegate.autoSignIn( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + } } 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 082eef794e..2b06e00018 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 @@ -20,7 +20,6 @@ import android.content.Intent import androidx.annotation.WorkerThread import aws.sdk.kotlin.services.cognitoidentityprovider.associateSoftwareToken import aws.sdk.kotlin.services.cognitoidentityprovider.confirmForgotPassword -import aws.sdk.kotlin.services.cognitoidentityprovider.confirmSignUp import aws.sdk.kotlin.services.cognitoidentityprovider.getUser import aws.sdk.kotlin.services.cognitoidentityprovider.model.AnalyticsMetadataType import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType @@ -41,18 +40,17 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareToken import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.resendConfirmationCode import aws.sdk.kotlin.services.cognitoidentityprovider.setUserMfaPreference -import aws.sdk.kotlin.services.cognitoidentityprovider.signUp import aws.sdk.kotlin.services.cognitoidentityprovider.verifySoftwareToken import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AWSCredentials import com.amplifyframework.auth.AWSTemporaryCredentials -import com.amplifyframework.auth.AuthCategoryBehavior import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser @@ -72,6 +70,7 @@ import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.HostedUIHelper import com.amplifyframework.auth.cognito.helpers.SessionHelper import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.collectWhile import com.amplifyframework.auth.cognito.helpers.getAllowedMFATypesFromChallengeParameters import com.amplifyframework.auth.cognito.helpers.getMFASetupTypeOrNull import com.amplifyframework.auth.cognito.helpers.getMFAType @@ -126,10 +125,8 @@ import com.amplifyframework.auth.result.AuthSignOutResult import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.AuthUpdateAttributeResult import com.amplifyframework.auth.result.step.AuthNextSignInStep -import com.amplifyframework.auth.result.step.AuthNextSignUpStep import com.amplifyframework.auth.result.step.AuthNextUpdateAttributeStep import com.amplifyframework.auth.result.step.AuthSignInStep -import com.amplifyframework.auth.result.step.AuthSignUpStep import com.amplifyframework.auth.result.step.AuthUpdateAttributeStep import com.amplifyframework.core.Action import com.amplifyframework.core.Amplify @@ -145,6 +142,9 @@ 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.SignUpData +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 @@ -153,7 +153,9 @@ import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent 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.events.SignUpEvent import com.amplifyframework.statemachine.codegen.states.AuthState import com.amplifyframework.statemachine.codegen.states.AuthenticationState import com.amplifyframework.statemachine.codegen.states.AuthorizationState @@ -164,6 +166,9 @@ 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.SignUpState +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 @@ -173,6 +178,10 @@ import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch internal class RealAWSCognitoAuthPlugin( @@ -180,7 +189,7 @@ internal class RealAWSCognitoAuthPlugin( private val authEnvironment: AuthEnvironment, private val authStateMachine: AuthStateMachine, private val logger: Logger -) : AuthCategoryBehavior { +) { private val lastPublishedHubEventName = AtomicReference() @@ -222,24 +231,12 @@ internal class RealAWSCognitoAuthPlugin( } internal suspend fun suspendWhileConfiguring() { - return suspendCoroutine { continuation -> - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { - if (it is AuthState.Configured || it is AuthState.Error) { - authStateMachine.cancel(token) - continuation.resume(Unit) - } - }, - { } - ) - } + authStateMachine.state.takeWhile { it !is AuthState.Configured && it !is AuthState.Error }.collect() } - override fun signUp( + fun signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions, onSuccess: Consumer, onError: Consumer @@ -259,92 +256,39 @@ internal class RealAWSCognitoAuthPlugin( private suspend fun _signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions, onSuccess: Consumer, onError: Consumer ) { - logger.verbose("SignUp Starting execution") - try { - val userAttributes = options.userAttributes.map { - AttributeType { - name = it.key.keyString - value = it.value + authStateMachine.state.onStart { + val validationData = (options as? AWSCognitoAuthSignUpOptions)?.validationData + val clientMetadata = (options as? AWSCognitoAuthSignUpOptions)?.clientMetadata + val signupData = SignUpData(username, validationData, clientMetadata) + val event = SignUpEvent(SignUpEvent.EventType.InitiateSignUp(signupData, password, options.userAttributes)) + authStateMachine.send(event) + }.drop(1).collectWhile { authState -> + when (val signUpState = authState.authSignUpState) { + is SignUpState.AwaitingUserConfirmation -> { + onSuccess.accept(signUpState.signUpResult) + false } - } - - val encodedContextData = authEnvironment.getUserContextData(username) - val pinpointEndpointId = authEnvironment.getPinpointEndpointId() - - val response = authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.signUp { - this.username = username - this.password = password - this.userAttributes = userAttributes - this.clientId = configuration.userPool?.appClient - this.secretHash = AuthHelper.getSecretHash( - username, - configuration.userPool?.appClient, - configuration.userPool?.appClientSecret - ) - pinpointEndpointId?.let { - this.analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = it } + is SignUpState.SignedUp -> { + onSuccess.accept(signUpState.signUpResult) + false } - encodedContextData?.let { this.userContextData { encodedData = it } } - - (options as? AWSCognitoAuthSignUpOptions)?.let { - this.validationData = it.validationData.mapNotNull { option -> - AttributeType { - name = option.key - value = option.value - } - } - this.clientMetadata = it.clientMetadata + is SignUpState.Error -> { + onError.accept( + CognitoAuthExceptionConverter.lookup(signUpState.exception, "Sign up failed.") + ) + false } + else -> true } - - val deliveryDetails = response?.codeDeliveryDetails?.let { details -> - mapOf( - "DESTINATION" to details.destination, - "MEDIUM" to details.deliveryMedium?.value, - "ATTRIBUTE" to details.attributeName - ) - } - - val authSignUpResult = if (response?.userConfirmed == true) { - AuthSignUpResult( - true, - AuthNextSignUpStep( - AuthSignUpStep.DONE, - mapOf(), - null - ), - response.userSub - ) - } else { - AuthSignUpResult( - false, - AuthNextSignUpStep( - AuthSignUpStep.CONFIRM_SIGN_UP_STEP, - mapOf(), - AuthCodeDeliveryDetails( - deliveryDetails?.getValue("DESTINATION") ?: "", - AuthCodeDeliveryDetails.DeliveryMedium.fromString( - deliveryDetails?.getValue("MEDIUM") - ), - deliveryDetails?.getValue("ATTRIBUTE") - ) - ), - response?.userSub - ) - } - onSuccess.accept(authSignUpResult) - logger.verbose("SignUp Execution complete") - } catch (exception: Exception) { - onError.accept(CognitoAuthExceptionConverter.lookup(exception, "Sign up failed.")) } } - override fun confirmSignUp( + fun confirmSignUp( username: String, confirmationCode: String, onSuccess: Consumer, @@ -353,7 +297,7 @@ internal class RealAWSCognitoAuthPlugin( confirmSignUp(username, confirmationCode, AuthConfirmSignUpOptions.defaults(), onSuccess, onError) } - override fun confirmSignUp( + fun confirmSignUp( username: String, confirmationCode: String, options: AuthConfirmSignUpOptions, @@ -366,7 +310,7 @@ internal class RealAWSCognitoAuthPlugin( InvalidUserPoolConfigurationException() ) is AuthenticationState.SignedIn, is AuthenticationState.SignedOut -> GlobalScope.launch { - _confirmSignUp(username, confirmationCode, options, onSuccess, onError) + _confirmSignUp(username, confirmationCode, authState.authSignUpState, options, onSuccess, onError) } else -> onError.accept(InvalidStateException()) } @@ -376,49 +320,129 @@ internal class RealAWSCognitoAuthPlugin( private suspend fun _confirmSignUp( username: String, confirmationCode: String, + authSignUpState: SignUpState?, options: AuthConfirmSignUpOptions, onSuccess: Consumer, onError: Consumer ) { - logger.verbose("ConfirmSignUp Starting execution") - try { - val encodedContextData = authEnvironment.getUserContextData(username) - val pinpointEndpointId = authEnvironment.getPinpointEndpointId() + val token = StateChangeListenerToken() + authStateMachine.listen( + token, + { authState -> + when (val signUpState = authState.authSignUpState) { + // Only process error if new. Existing errors have already been passed to customer + is SignUpState.Error -> { + if (signUpState.hasNewResponse) { + signUpState.hasNewResponse = false + authStateMachine.cancel(token) + onError.accept( + CognitoAuthExceptionConverter.lookup(signUpState.exception, "Sign up failed.") + ) + } + } + is SignUpState.SignedUp -> { + authStateMachine.cancel(token) + onSuccess.accept(signUpState.signUpResult) + } + else -> Unit + } + }, + { + var userId: String? = null + var session: String? = null + if (authSignUpState is SignUpState.AwaitingUserConfirmation && + authSignUpState.signUpData.username == username + ) { + session = authSignUpState.signUpData.session + userId = authSignUpState.signUpResult.userId + } + val clientMetadata = (options as? AWSCognitoAuthConfirmSignUpOptions)?.clientMetadata + val signupData = SignUpData(username, null, clientMetadata, session, userId) + val event = SignUpEvent(SignUpEvent.EventType.ConfirmSignUp(signupData, confirmationCode)) + authStateMachine.send(event) + } + ) + } - authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.confirmSignUp { - this.username = username - this.confirmationCode = confirmationCode - this.clientId = configuration.userPool?.appClient - this.secretHash = AuthHelper.getSecretHash( - username, - configuration.userPool?.appClient, - configuration.userPool?.appClientSecret + fun autoSignIn(onSuccess: Consumer, onError: Consumer) { + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + is AuthenticationState.NotConfigured -> onError.accept( + InvalidUserPoolConfigurationException() ) - pinpointEndpointId?.let { - this.analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = it } + is AuthenticationState.SignedIn -> { + onError.accept(InvalidStateException()) } - encodedContextData?.let { this.userContextData { encodedData = it } } - - (options as? AWSCognitoAuthConfirmSignUpOptions)?.let { - this.clientMetadata = it.clientMetadata + is AuthenticationState.SignedOut -> GlobalScope.launch { + when (val signUpState = authState.authSignUpState) { + is SignUpState.SignedUp -> { + _autoSignIn(signUpState.signUpData, onSuccess, onError) + } + else -> onError.accept(InvalidStateException()) + } } + else -> onError.accept(InvalidStateException()) } - - val authSignUpResult = AuthSignUpResult( - true, - AuthNextSignUpStep(AuthSignUpStep.DONE, mapOf(), null), - null - ) - onSuccess.accept(authSignUpResult) - logger.verbose("ConfirmSignUp Execution complete") - } catch (exception: Exception) { - onError.accept( - CognitoAuthExceptionConverter.lookup(exception, "Confirm sign up failed.") - ) } } - override fun resendSignUpCode( + private suspend fun _autoSignIn( + signUpData: SignUpData, + onSuccess: Consumer, + onError: Consumer + ) { + val token = StateChangeListenerToken() + authStateMachine.listen( + token, + { authState -> + val authNState = authState.authNState + val authZState = authState.authZState + when { + authNState is AuthenticationState.SigningIn -> { + val signInState = authNState.signInState + when { + signInState is SignInState.Error -> { + authStateMachine.cancel(token) + onError.accept( + CognitoAuthExceptionConverter.lookup(signInState.exception, "Sign in failed.") + ) + } + } + } + authNState is AuthenticationState.SignedIn && + authZState is AuthorizationState.SessionEstablished -> { + authStateMachine.cancel(token) + val authSignInResult = AuthSignInResult( + true, + AuthNextSignInStep( + AuthSignInStep.DONE, + mapOf(), + null, + null, + null, + null + ) + ) + onSuccess.accept(authSignInResult) + sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + } + else -> Unit + } + }, + { + val signInData = SignInData.AutoSignInData( + signUpData.username, + signUpData.session, + signUpData.clientMetadata ?: mapOf(), + signUpData.userId + ) + val event = AuthenticationEvent(AuthenticationEvent.EventType.SignInRequested(signInData)) + authStateMachine.send(event) + } + ) + } + + fun resendSignUpCode( username: String, onSuccess: Consumer, onError: Consumer @@ -426,7 +450,7 @@ internal class RealAWSCognitoAuthPlugin( resendSignUpCode(username, AuthResendSignUpCodeOptions.defaults(), onSuccess, onError) } - override fun resendSignUpCode( + fun resendSignUpCode( username: String, options: AuthResendSignUpCodeOptions, onSuccess: Consumer, @@ -494,7 +518,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun signIn( + fun signIn( username: String?, password: String?, onSuccess: Consumer, @@ -503,7 +527,7 @@ internal class RealAWSCognitoAuthPlugin( signIn(username, password, AuthSignInOptions.defaults(), onSuccess, onError) } - override fun signIn( + fun signIn( username: String?, password: String?, options: AuthSignInOptions, @@ -567,6 +591,7 @@ internal class RealAWSCognitoAuthPlugin( val srpSignInState = (signInState as? SignInState.SigningInWithSRP)?.srpSignInState val challengeState = (signInState as? SignInState.ResolvingChallenge)?.challengeState val totpSetupState = (signInState as? SignInState.ResolvingTOTPSetup)?.setupTOTPState + val webAuthnState = (signInState as? SignInState.SigningInWithWebAuthn)?.webAuthnSignInState when { srpSignInState is SRPSignInState.Error -> { authStateMachine.cancel(token) @@ -584,6 +609,12 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) SignInChallengeHelper.getNextStep(challengeState.challenge, onSuccess, onError) } + webAuthnState is WebAuthnSignInState.Error -> { + authStateMachine.cancel(token) + onError.accept( + CognitoAuthExceptionConverter.lookup(webAuthnState.exception, "Sign in failed") + ) + } totpSetupState is SetupTOTPState.WaitingForAnswer -> { authStateMachine.cancel(token) @@ -607,18 +638,27 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) } + authNState is AuthenticationState.Error -> { + authStateMachine.cancel(token) + val exception = if (authNState.exception is AuthException) { + authNState.exception + } else { + UnknownException(cause = authNState.exception) + } + onError.accept(exception) + } else -> Unit } }, { val signInData = when (options.authFlowType ?: configuration.authFlowType) { AuthFlowType.USER_SRP_AUTH -> { - SignInData.SRPSignInData(username, password, options.metadata) + SignInData.SRPSignInData(username, password, options.metadata, AuthFlowType.USER_SRP_AUTH) } AuthFlowType.CUSTOM_AUTH, AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP -> { SignInData.CustomAuthSignInData(username, options.metadata) @@ -627,7 +667,35 @@ internal class RealAWSCognitoAuthPlugin( SignInData.CustomSRPAuthSignInData(username, password, options.metadata) } AuthFlowType.USER_PASSWORD_AUTH -> { - SignInData.MigrationAuthSignInData(username, password, options.metadata) + 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)) @@ -636,7 +704,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun confirmSignIn( + fun confirmSignIn( challengeResponse: String, onSuccess: Consumer, onError: Consumer @@ -644,7 +712,7 @@ internal class RealAWSCognitoAuthPlugin( confirmSignIn(challengeResponse, AuthConfirmSignInOptions.defaults(), onSuccess, onError) } - override fun confirmSignIn( + fun confirmSignIn( challengeResponse: String, options: AuthConfirmSignInOptions, onSuccess: Consumer, @@ -667,6 +735,18 @@ internal class RealAWSCognitoAuthPlugin( 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 { @@ -696,7 +776,7 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) @@ -717,9 +797,11 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) val signInChallengeState = signInState.challengeState as SignInChallengeState.WaitingForAnswer var allowedMFATypes: Set? = null + var codeDeliveryDetails: AuthCodeDeliveryDetails? = null - if (signInChallengeState.challenge.challengeName == ChallengeNameType.MfaSetup.value || - signInChallengeState.challenge.challengeName == ChallengeNameType.EmailOtp.value + if (signInChallengeState.challenge.challengeNameType == ChallengeNameType.MfaSetup || + signInChallengeState.challenge.challengeNameType == ChallengeNameType.EmailOtp || + signInChallengeState.challenge.challengeNameType == ChallengeNameType.SmsOtp ) { SignInChallengeHelper.getNextStep( signInChallengeState.challenge, @@ -730,15 +812,32 @@ internal class RealAWSCognitoAuthPlugin( return@listen } - val signInStep = when (signInChallengeState.challenge.challengeName) { - "SMS_MFA" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE - "NEW_PASSWORD_REQUIRED" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD - "SOFTWARE_TOKEN_MFA" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE - "SELECT_MFA_TYPE" -> { + val signInStep = when (signInChallengeState.challenge.challengeNameType) { + ChallengeNameType.SmsMfa -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE + ChallengeNameType.NewPasswordRequired -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD + ChallengeNameType.SoftwareTokenMfa -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE + ChallengeNameType.SelectMfaType -> { allowedMFATypes = getAllowedMFATypesFromChallengeParameters(signInChallengeState.challenge.parameters) AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION } + ChallengeNameType.EmailOtp, ChallengeNameType.SmsOtp -> { + signInChallengeState.challenge.parameters?.get( + "CODE_DELIVERY_DELIVERY_MEDIUM" + )?.let { medium -> + signInChallengeState.challenge.parameters["CODE_DELIVERY_DESTINATION"] + ?.let { destination -> + codeDeliveryDetails = AuthCodeDeliveryDetails( + destination, + AuthCodeDeliveryDetails.DeliveryMedium.fromString(medium) + ) + } + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP + } + ChallengeNameType.Password, ChallengeNameType.PasswordSrp -> { + AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD + } else -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE } val authSignInResult = AuthSignInResult( @@ -746,9 +845,10 @@ internal class RealAWSCognitoAuthPlugin( AuthNextSignInStep( signInStep, signInChallengeState.challenge.parameters ?: mapOf(), + codeDeliveryDetails, null, - null, - allowedMFATypes + allowedMFATypes, + null ) ) onSuccess.accept(authSignInResult) @@ -800,6 +900,17 @@ internal class RealAWSCognitoAuthPlugin( ) (signInState.challengeState as SignInChallengeState.Error).hasNewResponse = false } + + signInState is SignInState.SigningInWithWebAuthn && + signInState.webAuthnSignInState is WebAuthnSignInState.Error && + (signInState.webAuthnSignInState as WebAuthnSignInState.Error).hasNewResponse -> { + val errorState = signInState.webAuthnSignInState as WebAuthnSignInState.Error + authStateMachine.cancel(token) + onError.accept( + CognitoAuthExceptionConverter.lookup(errorState.exception, "Confirm Sign in failed.") + ) + errorState.hasNewResponse = false + } } }, { @@ -810,7 +921,7 @@ internal class RealAWSCognitoAuthPlugin( is SignInState.ResolvingChallenge -> { val challengeState = signInState.challengeState if (challengeState is SignInChallengeState.WaitingForAnswer && - challengeState.challenge.challengeName == "SELECT_MFA_TYPE" && + challengeState.challenge.challengeNameType == ChallengeNameType.SelectMfaType && getMFATypeOrNull(challengeResponse) == null ) { val error = InvalidParameterException( @@ -818,6 +929,7 @@ internal class RealAWSCognitoAuthPlugin( "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 @@ -827,6 +939,87 @@ internal class RealAWSCognitoAuthPlugin( ) 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 + ) + ) + ) + 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 + ) + ) + ) + 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( @@ -872,17 +1065,36 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.send(event) } - else -> onError.accept(InvalidStateException()) + else -> { + onError.accept(InvalidStateException()) + authStateMachine.cancel(token) + } } } - else -> onError.accept(InvalidStateException()) + 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) + } } } ) } - override fun signInWithSocialWebUI( + fun signInWithSocialWebUI( provider: AuthProvider, callingActivity: Activity, onSuccess: Consumer, @@ -897,7 +1109,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun signInWithSocialWebUI( + fun signInWithSocialWebUI( provider: AuthProvider, callingActivity: Activity, options: AuthWebUISignInOptions, @@ -913,7 +1125,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun signInWithWebUI( + fun signInWithWebUI( callingActivity: Activity, onSuccess: Consumer, onError: Consumer @@ -921,7 +1133,7 @@ internal class RealAWSCognitoAuthPlugin( signInWithWebUI(callingActivity, AuthWebUISignInOptions.builder().build(), onSuccess, onError) } - override fun signInWithWebUI( + fun signInWithWebUI( callingActivity: Activity, options: AuthWebUISignInOptions, onSuccess: Consumer, @@ -1027,7 +1239,7 @@ internal class RealAWSCognitoAuthPlugin( val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) @@ -1048,13 +1260,14 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun handleWebUISignInResponse(intent: Intent?) { + fun handleWebUISignInResponse(intent: Intent?) { authStateMachine.getCurrentState { val callbackUri = intent?.data when (val authNState = it.authNState) { is AuthenticationState.SigningOut -> { (authNState.signOutState as? SignOutState.SigningOutHostedUI)?.let { signOutState -> - if (callbackUri == null && !signOutState.bypassCancel && + if (callbackUri == null && + !signOutState.bypassCancel && signOutState.signedInData.signInMethod != SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.UNKNOWN) ) { @@ -1121,30 +1334,28 @@ internal class RealAWSCognitoAuthPlugin( } } - private suspend fun getSession(): AWSCognitoAuthSession { - return suspendCoroutine { continuation -> - fetchAuthSession( - { authSession -> - if (authSession is AWSCognitoAuthSession) { - continuation.resume(authSession) - } else { - continuation.resumeWithException( - UnknownException( - message = "fetchAuthSession did not return a type of AWSCognitoAuthSession" - ) + private suspend fun getSession(): AWSCognitoAuthSession = suspendCoroutine { continuation -> + fetchAuthSession( + { authSession -> + if (authSession is AWSCognitoAuthSession) { + continuation.resume(authSession) + } else { + continuation.resumeWithException( + UnknownException( + message = "fetchAuthSession did not return a type of AWSCognitoAuthSession" ) - } - }, - { continuation.resumeWithException(it) } - ) - } + ) + } + }, + { continuation.resumeWithException(it) } + ) } - override fun fetchAuthSession(onSuccess: Consumer, onError: Consumer) { + fun fetchAuthSession(onSuccess: Consumer, onError: Consumer) { fetchAuthSession(AuthFetchSessionOptions.defaults(), onSuccess, onError) } - override fun fetchAuthSession( + fun fetchAuthSession( options: AuthFetchSessionOptions, onSuccess: Consumer, onError: Consumer @@ -1208,9 +1419,7 @@ internal class RealAWSCognitoAuthPlugin( } } - private fun _fetchAuthSession( - onSuccess: Consumer - ) { + private fun _fetchAuthSession(onSuccess: Consumer) { val token = StateChangeListenerToken() authStateMachine.listen( token, @@ -1261,7 +1470,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun rememberDevice(onSuccess: Action, onError: Consumer) { + fun rememberDevice(onSuccess: Action, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (val state = authState.authNState) { is AuthenticationState.SignedIn -> { @@ -1284,7 +1493,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun forgetDevice(onSuccess: Action, onError: Consumer) { + fun forgetDevice(onSuccess: Action, onError: Consumer) { forgetDevice(AuthDevice.fromId(""), onSuccess, onError) } @@ -1311,11 +1520,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun forgetDevice( - device: AuthDevice, - onSuccess: Action, - onError: Consumer - ) { + fun forgetDevice(device: AuthDevice, onSuccess: Action, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (val authNState = authState.authNState) { is AuthenticationState.SignedIn -> { @@ -1354,10 +1559,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun fetchDevices( - onSuccess: Consumer>, - onError: Consumer - ) { + fun fetchDevices(onSuccess: Consumer>, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.SignedIn -> { @@ -1398,7 +1600,7 @@ internal class RealAWSCognitoAuthPlugin( } @OptIn(DelicateCoroutinesApi::class) - override fun resetPassword( + fun resetPassword( username: String, options: AuthResetPasswordOptions, onSuccess: Consumer, @@ -1432,7 +1634,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun resetPassword( + fun resetPassword( username: String, onSuccess: Consumer, onError: Consumer @@ -1440,7 +1642,7 @@ internal class RealAWSCognitoAuthPlugin( resetPassword(username, AuthResetPasswordOptions.defaults(), onSuccess, onError) } - override fun confirmResetPassword( + fun confirmResetPassword( username: String, newPassword: String, confirmationCode: String, @@ -1490,7 +1692,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun confirmResetPassword( + fun confirmResetPassword( username: String, newPassword: String, confirmationCode: String, @@ -1507,12 +1709,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun updatePassword( - oldPassword: String, - newPassword: String, - onSuccess: Action, - onError: Consumer - ) { + fun updatePassword(oldPassword: String, newPassword: String, onSuccess: Action, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { // Check if user signed in @@ -1550,10 +1747,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun fetchUserAttributes( - onSuccess: Consumer>, - onError: Consumer - ) { + fun fetchUserAttributes(onSuccess: Consumer>, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { // Check if user signed in @@ -1589,7 +1783,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun updateUserAttribute( + fun updateUserAttribute( attribute: AuthUserAttribute, options: AuthUpdateUserAttributeOptions, onSuccess: Consumer, @@ -1609,7 +1803,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun updateUserAttribute( + fun updateUserAttribute( attribute: AuthUserAttribute, onSuccess: Consumer, onError: Consumer @@ -1617,7 +1811,7 @@ internal class RealAWSCognitoAuthPlugin( updateUserAttribute(attribute, AuthUpdateUserAttributeOptions.defaults(), onSuccess, onError) } - override fun updateUserAttributes( + fun updateUserAttributes( attributes: List, options: AuthUpdateUserAttributesOptions, onSuccess: Consumer>, @@ -1635,7 +1829,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun updateUserAttributes( + fun updateUserAttributes( attributes: List, onSuccess: Consumer>, onError: Consumer @@ -1646,47 +1840,45 @@ internal class RealAWSCognitoAuthPlugin( private suspend fun updateUserAttributes( attributes: List, userAttributesOptionsMetadata: Map? - ): MutableMap { - return suspendCoroutine { continuation -> + ): MutableMap = suspendCoroutine { continuation -> - authStateMachine.getCurrentState { authState -> - when (authState.authNState) { - // Check if user signed in - is AuthenticationState.SignedIn -> { - GlobalScope.launch { - try { - val accessToken = getSession().userPoolTokensResult.value?.accessToken - accessToken?.let { - var userAttributes = attributes.map { - AttributeType.invoke { - name = it.key.keyString - value = it.value - } - } - val userAttributesRequest = UpdateUserAttributesRequest.invoke { - this.accessToken = accessToken - this.userAttributes = userAttributes - this.clientMetadata = userAttributesOptionsMetadata + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + // Check if user signed in + is AuthenticationState.SignedIn -> { + GlobalScope.launch { + try { + val accessToken = getSession().userPoolTokensResult.value?.accessToken + accessToken?.let { + var userAttributes = attributes.map { + AttributeType.invoke { + name = it.key.keyString + value = it.value } - val userAttributeResponse = authEnvironment.cognitoAuthService - .cognitoIdentityProviderClient?.updateUserAttributes( - userAttributesRequest - ) - - continuation.resume( - getUpdateUserAttributeResult(userAttributeResponse, userAttributes) + } + val userAttributesRequest = UpdateUserAttributesRequest.invoke { + this.accessToken = accessToken + this.userAttributes = userAttributes + this.clientMetadata = userAttributesOptionsMetadata + } + val userAttributeResponse = authEnvironment.cognitoAuthService + .cognitoIdentityProviderClient?.updateUserAttributes( + userAttributesRequest ) - } ?: continuation.resumeWithException( - InvalidUserPoolConfigurationException() + + continuation.resume( + getUpdateUserAttributeResult(userAttributeResponse, userAttributes) ) - } catch (e: Exception) { - continuation.resumeWithException(CognitoAuthExceptionConverter.lookup(e, e.toString())) - } + } ?: continuation.resumeWithException( + InvalidUserPoolConfigurationException() + ) + } catch (e: Exception) { + continuation.resumeWithException(CognitoAuthExceptionConverter.lookup(e, e.toString())) } } - is AuthenticationState.SignedOut -> continuation.resumeWithException(SignedOutException()) - else -> continuation.resumeWithException(InvalidStateException()) } + is AuthenticationState.SignedOut -> continuation.resumeWithException(SignedOutException()) + else -> continuation.resumeWithException(InvalidStateException()) } } } @@ -1733,7 +1925,7 @@ internal class RealAWSCognitoAuthPlugin( return finalResult } - override fun resendUserAttributeConfirmationCode( + fun resendUserAttributeConfirmationCode( attributeKey: AuthUserAttributeKey, options: AuthResendUserAttributeConfirmationCodeOptions, onSuccess: Consumer, @@ -1790,7 +1982,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun resendUserAttributeConfirmationCode( + fun resendUserAttributeConfirmationCode( attributeKey: AuthUserAttributeKey, onSuccess: Consumer, onError: Consumer @@ -1803,7 +1995,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun confirmUserAttribute( + fun confirmUserAttribute( attributeKey: AuthUserAttributeKey, confirmationCode: String, onSuccess: Action, @@ -1839,10 +2031,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun getCurrentUser( - onSuccess: Consumer, - onError: Consumer - ) { + fun getCurrentUser(onSuccess: Consumer, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.SignedIn -> { @@ -1871,11 +2060,11 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun signOut(onComplete: Consumer) { + fun signOut(onComplete: Consumer) { signOut(AuthSignOutOptions.builder().build(), onComplete) } - override fun signOut(options: AuthSignOutOptions, onComplete: Consumer) { + fun signOut(options: AuthSignOutOptions, onComplete: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.NotConfigured -> @@ -1979,7 +2168,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - override fun deleteUser(onSuccess: Action, onError: Consumer) { + fun deleteUser(onSuccess: Action, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.SignedIn -> { @@ -2086,11 +2275,12 @@ internal class RealAWSCognitoAuthPlugin( authNState is AuthenticationState.Error || authNState is AuthenticationState.NotConfigured || authNState is AuthenticationState.FederatedToIdentityPool - ) && ( - authZState is AuthorizationState.Configured || - authZState is AuthorizationState.SessionEstablished || - authZState is AuthorizationState.Error - ) -> { + ) && + ( + authZState is AuthorizationState.Configured || + authZState is AuthorizationState.SessionEstablished || + authZState is AuthorizationState.Error + ) -> { val existingCredential = when (authZState) { is AuthorizationState.SessionEstablished -> authZState.amplifyCredential is AuthorizationState.Error -> { @@ -2171,10 +2361,7 @@ internal class RealAWSCognitoAuthPlugin( ) } - fun clearFederationToIdentityPool( - onSuccess: Action, - onError: Consumer - ) { + fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer) { authStateMachine.getCurrentState { authState -> val authNState = authState.authNState val authZState = authState.authZState @@ -2183,11 +2370,12 @@ internal class RealAWSCognitoAuthPlugin( ( authNState is AuthenticationState.FederatedToIdentityPool && authZState is AuthorizationState.SessionEstablished - ) || ( - authZState is AuthorizationState.Error && - authZState.exception is SessionError && - authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated - ) -> { + ) || + ( + authZState is AuthorizationState.Error && + authZState.exception is SessionError && + authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated + ) -> { val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool()) authStateMachine.send(event) _clearFederationToIdentityPool(onSuccess, onError) @@ -2199,7 +2387,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun setUpTOTP(onSuccess: Consumer, onError: Consumer) { + fun setUpTOTP(onSuccess: Consumer, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.SignedIn -> { @@ -2239,15 +2427,7 @@ internal class RealAWSCognitoAuthPlugin( } } - override fun verifyTOTPSetup( - code: String, - onSuccess: Action, - onError: Consumer - ) { - verifyTotp(code, null, onSuccess, onError) - } - - override fun verifyTOTPSetup( + fun verifyTOTPSetup( code: String, options: AuthVerifyTOTPSetupOptions, onSuccess: Action, @@ -2257,10 +2437,7 @@ internal class RealAWSCognitoAuthPlugin( verifyTotp(code, cognitoOptions?.friendlyDeviceName, onSuccess, onError) } - fun fetchMFAPreference( - onSuccess: Consumer, - onError: Consumer - ) { + fun fetchMFAPreference(onSuccess: Consumer, onError: Consumer) { authStateMachine.getCurrentState { authState -> when (authState.authNState) { is AuthenticationState.SignedIn -> { @@ -2314,8 +2491,7 @@ internal class RealAWSCognitoAuthPlugin( return } // If either of the params have preferred setting set then ignore fetched preference preferred property - val overridePreferredSetting = - !(sms?.mfaPreferred == true || totp?.mfaPreferred == true || email?.mfaPreferred == true) + val overridePreferredSetting: Boolean = !(sms?.mfaPreferred == true || totp?.mfaPreferred == true) fetchMFAPreference({ userPreference -> authStateMachine.getCurrentState { authState -> when (authState.authNState) { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/AuthenticationCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/AuthenticationCognitoActions.kt index a4aa8b6fe1..f80996c864 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/AuthenticationCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/AuthenticationCognitoActions.kt @@ -72,7 +72,12 @@ internal object AuthenticationCognitoActions : AuthenticationActions { is SignInData.SRPSignInData -> { if (data.username != null && data.password != null) { SignInEvent( - SignInEvent.EventType.InitiateSignInWithSRP(data.username, data.password, data.metadata) + SignInEvent.EventType.InitiateSignInWithSRP( + data.username, + data.password, + data.metadata, + data.authFlowType + ) ) } else { AuthenticationEvent( @@ -118,7 +123,12 @@ internal object AuthenticationCognitoActions : AuthenticationActions { is SignInData.MigrationAuthSignInData -> { if (data.username != null && data.password != null) { SignInEvent( - SignInEvent.EventType.InitiateMigrateAuth(data.username, data.password, data.metadata) + SignInEvent.EventType.InitiateMigrateAuth( + username = data.username, + password = data.password, + metadata = data.metadata, + authFlowType = data.authFlowType + ) ) } else { AuthenticationEvent( @@ -128,6 +138,28 @@ internal object AuthenticationCognitoActions : AuthenticationActions { ) } } + is SignInData.UserAuthSignInData -> { + if (data.username != null) { + SignInEvent( + SignInEvent.EventType.InitiateUserAuth( + data.username, + data.preferredChallenge, + data.callingActivity, + data.metadata + ) + ) + } else { + AuthenticationEvent( + AuthenticationEvent.EventType.ThrowError( + ValidationException("Sign in failed.", "username cannot be empty") + ) + ) + } + } + + is SignInData.AutoSignInData -> { + SignInEvent(SignInEvent.EventType.InitiateAutoSignIn(data)) + } } logger.verbose("$id Sending event ${evt.type}") diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActions.kt index 273dae179c..dfb19bf8cc 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActions.kt @@ -16,14 +16,19 @@ package com.amplifyframework.auth.cognito.actions import aws.sdk.kotlin.services.cognitoidentityprovider.initiateAuth -import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.respondToAuthChallenge +import aws.smithy.kotlin.runtime.util.type import com.amplifyframework.AmplifyException import com.amplifyframework.auth.cognito.AuthEnvironment import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.toCognitoType +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.exceptions.ServiceException import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.MigrateAuthActions +import com.amplifyframework.statemachine.codegen.data.SignInMethod import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent @@ -32,6 +37,8 @@ internal object MigrateAuthCognitoActions : MigrateAuthActions { private const val KEY_PASSWORD = "PASSWORD" private const val KEY_SECRET_HASH = "SECRET_HASH" private const val KEY_USERID_FOR_SRP = "USER_ID_FOR_SRP" + private const val KEY_ANSWER = "ANSWER" + private const val KEY_PREFERRED_CHALLENGE = "PREFERRED_CHALLENGE" override fun initiateMigrateAuthAction(event: SignInEvent.EventType.InitiateMigrateAuth) = Action("InitMigrateAuth") { id, dispatcher -> @@ -49,32 +56,64 @@ internal object MigrateAuthCognitoActions : MigrateAuthActions { val encodedContextData = getUserContextData(event.username) val pinpointEndpointId = getPinpointEndpointId() - val response = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { - authFlow = AuthFlowType.UserPasswordAuth - clientId = configuration.userPool?.appClient - authParameters = authParams - clientMetadata = event.metadata - pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } - encodedContextData?.let { userContextData { encodedData = it } } - } + if (event.respondToAuthChallenge?.session != null) { + authParams[KEY_ANSWER] = ChallengeNameType.Password.value + + val response = cognitoAuthService.cognitoIdentityProviderClient?.respondToAuthChallenge { + clientId = configuration.userPool?.appClient + challengeName = ChallengeNameType.SelectChallenge + this.challengeResponses = authParams + session = event.respondToAuthChallenge.session + clientMetadata = event.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { this.userContextData { encodedData = it } } + } - if (response != null) { - val username = AuthHelper.getActiveUsername( - username = event.username, - alternateUsername = response.challengeParameters?.get(KEY_USERNAME), - userIDForSRP = response.challengeParameters?.get( - KEY_USERID_FOR_SRP + response?.let { + SignInChallengeHelper.evaluateNextStep( + username = event.username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult, + signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) ) - ) - SignInChallengeHelper.evaluateNextStep( - username, - response.challengeName, - response.session, - response.challengeParameters, - response.authenticationResult - ) + } ?: throw ServiceException("Sign in failed", AmplifyException.TODO_RECOVERY_SUGGESTION) } else { - throw ServiceException("Sign in failed", AmplifyException.TODO_RECOVERY_SUGGESTION) + if (event.authFlowType == AuthFlowType.USER_AUTH) { + authParams[KEY_PREFERRED_CHALLENGE] = KEY_PASSWORD + } + val response = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { + authFlow = event.authFlowType.toCognitoType() + clientId = configuration.userPool?.appClient + authParameters = authParams + clientMetadata = event.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + } + + response?.let { + val username = AuthHelper.getActiveUsername( + username = event.username, + alternateUsername = response.challengeParameters?.get(KEY_USERNAME), + userIDForSRP = response.challengeParameters?.get( + KEY_USERID_FOR_SRP + ) + ) + val signInMethod = if (event.authFlowType == AuthFlowType.USER_AUTH) { + SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + } else { + SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_SRP_AUTH) + } + SignInChallengeHelper.evaluateNextStep( + username = username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult, + signInMethod = signInMethod + ) + } ?: throw ServiceException("Sign in failed", AmplifyException.TODO_RECOVERY_SUGGESTION) } } catch (e: Exception) { val errorEvent = SignInEvent(SignInEvent.EventType.ThrowError(e)) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt index c5160cbddb..6217be9d9d 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt @@ -16,7 +16,6 @@ package com.amplifyframework.auth.cognito.actions import aws.sdk.kotlin.services.cognitoidentityprovider.initiateAuth -import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResourceNotFoundException import aws.sdk.kotlin.services.cognitoidentityprovider.respondToAuthChallenge @@ -25,10 +24,14 @@ import com.amplifyframework.auth.cognito.AuthEnvironment import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.toCognitoType +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.exceptions.ServiceException +import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.SRPActions import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.SRPEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent @@ -48,6 +51,9 @@ internal object SRPCognitoActions : SRPActions { private const val KEY_USERID_FOR_SRP = "USER_ID_FOR_SRP" private const val KEY_DEVICE_KEY = "DEVICE_KEY" private const val KEY_CHALLENGE_NAME = "CHALLENGE_NAME" + private const val KEY_ANSWER = "ANSWER" + private const val KEY_PREFERRED_CHALLENGE = "PREFERRED_CHALLENGE" + private const val KEY_PASSWORD_SRP = "PASSWORD_SRP" override fun initiateSRPAuthAction(event: SRPEvent.EventType.InitiateSRP) = Action("InitSRPAuth") { id, dispatcher -> @@ -69,42 +75,67 @@ internal object SRPCognitoActions : SRPActions { deviceMetadata?.let { authParams[KEY_DEVICE_KEY] = it.deviceKey } val pinpointEndpointId = getPinpointEndpointId() - val initiateAuthResponse = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { - authFlow = AuthFlowType.UserSrpAuth - clientId = configuration.userPool?.appClient - authParameters = authParams - clientMetadata = event.metadata - pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } - encodedContextData?.let { userContextData { encodedData = it } } - } + if (event.respondToAuthChallenge?.session != null) { - when (initiateAuthResponse?.challengeName) { - ChallengeNameType.PasswordVerifier -> { - val updatedDeviceMetadata = getDeviceMetadata( - AuthHelper.getActiveUsername( - username = event.username, - alternateUsername = initiateAuthResponse.challengeParameters?.get(KEY_USERNAME), - userIDForSRP = initiateAuthResponse.challengeParameters?.get( - KEY_USERID_FOR_SRP - ) + authParams[KEY_ANSWER] = ChallengeNameType.PasswordSrp.value + + val response = cognitoAuthService.cognitoIdentityProviderClient?.respondToAuthChallenge { + clientId = configuration.userPool?.appClient + challengeName = ChallengeNameType.SelectChallenge + this.challengeResponses = authParams + session = event.respondToAuthChallenge.session + clientMetadata = event.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { this.userContextData { encodedData = it } } + } + + val updatedDeviceMetadata = getDeviceMetadata( + AuthHelper.getActiveUsername( + username = event.username, + alternateUsername = response?.challengeParameters?.get(KEY_USERNAME), + userIDForSRP = response?.challengeParameters?.get( + KEY_USERID_FOR_SRP ) ) + ) - initiateAuthResponse.challengeParameters?.let { params -> - val challengeParams = updatedDeviceMetadata?.deviceKey?.let { - params.plus(KEY_DEVICE_KEY to it) - } ?: params + parseResponseChallenge( + challengeNameType = response?.challengeName, + challengeParams = response?.challengeParameters, + session = response?.session, + updatedDeviceMetadata = updatedDeviceMetadata, + metadata = event.metadata + ) + } else { + if (event.authFlowType == AuthFlowType.USER_AUTH) { + authParams[KEY_PREFERRED_CHALLENGE] = KEY_PASSWORD_SRP + } + val initiateAuthResponse = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { + authFlow = event.authFlowType.toCognitoType() + clientId = configuration.userPool?.appClient + authParameters = authParams + clientMetadata = event.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + } - SRPEvent( - SRPEvent.EventType.RespondPasswordVerifier( - challengeParams, - event.metadata, - initiateAuthResponse.session - ) + val updatedDeviceMetadata = getDeviceMetadata( + AuthHelper.getActiveUsername( + username = event.username, + alternateUsername = initiateAuthResponse?.challengeParameters?.get(KEY_USERNAME), + userIDForSRP = initiateAuthResponse?.challengeParameters?.get( + KEY_USERID_FOR_SRP ) - } ?: throw Exception("Auth challenge parameters are empty.") - } - else -> throw Exception("Not yet implemented.") + ) + ) + + parseResponseChallenge( + challengeNameType = initiateAuthResponse?.challengeName, + challengeParams = initiateAuthResponse?.challengeParameters, + session = initiateAuthResponse?.session, + updatedDeviceMetadata = updatedDeviceMetadata, + metadata = event.metadata + ) } } catch (e: Exception) { val errorEvent = SRPEvent(SRPEvent.EventType.ThrowAuthError(e)) @@ -117,6 +148,35 @@ internal object SRPCognitoActions : SRPActions { dispatcher.send(evt) } + private fun parseResponseChallenge( + challengeNameType: ChallengeNameType?, + challengeParams: Map?, + session: String?, + updatedDeviceMetadata: DeviceMetadata.Metadata?, + metadata: Map + ): SRPEvent = + when (challengeNameType) { + ChallengeNameType.PasswordVerifier -> { + challengeParams?.let { params -> + val updatedChallengeParams = updatedDeviceMetadata?.deviceKey?.let { + params.plus(KEY_DEVICE_KEY to it) + } ?: params + + SRPEvent( + SRPEvent.EventType.RespondPasswordVerifier( + updatedChallengeParams, + metadata, + session + ) + ) + } ?: throw ServiceException( + "Auth challenge parameters are empty.", + AmplifyException.TODO_RECOVERY_SUGGESTION + ) + } + else -> throw UnknownException(cause = Exception("Challenge type not supported for this flow.")) + } + override fun initiateSRPWithCustomAuthAction(event: SRPEvent.EventType.InitiateSRPWithCustom): Action = Action("InitSRPCustomAuth") { id, dispatcher -> logger.verbose("$id Starting execution") @@ -142,7 +202,7 @@ internal object SRPCognitoActions : SRPActions { val pinpointEndpointId = getPinpointEndpointId() val initiateAuthResponse = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { - authFlow = AuthFlowType.CustomAuth + authFlow = AuthFlowType.CUSTOM_AUTH.toCognitoType() clientId = configuration.userPool?.appClient authParameters = authParams clientMetadata = event.metadata @@ -228,11 +288,11 @@ internal object SRPCognitoActions : SRPActions { } if (response != null) { SignInChallengeHelper.evaluateNextStep( - username, - response.challengeName, - response.session, - response.challengeParameters, - response.authenticationResult + username = username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult ) } else { throw ServiceException( diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt index 50738d5149..898f4ba385 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt @@ -29,6 +29,7 @@ import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.challengeNameType import com.amplifyframework.statemachine.codegen.events.CustomSignInEvent import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent @@ -88,7 +89,7 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { val pinpointEndpointId = getPinpointEndpointId() val response = cognitoAuthService.cognitoIdentityProviderClient?.respondToAuthChallenge { clientId = configuration.userPool?.appClient - challengeName = ChallengeNameType.fromValue(challenge.challengeName) + challengeName = challenge.challengeNameType this.challengeResponses = challengeResponses session = challenge.session clientMetadata = metadata @@ -130,13 +131,17 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { } private fun getChallengeResponseKey(challenge: AuthChallenge): String? { - val challengeName = challenge.challengeName - return when (ChallengeNameType.fromValue(challengeName)) { + return when (challenge.challengeNameType) { is ChallengeNameType.SmsMfa -> "SMS_MFA_CODE" + is ChallengeNameType.EmailOtp -> "EMAIL_OTP_CODE" + is ChallengeNameType.SmsOtp -> "SMS_OTP_CODE" is ChallengeNameType.NewPasswordRequired -> "NEW_PASSWORD" - is ChallengeNameType.CustomChallenge, ChallengeNameType.SelectMfaType -> "ANSWER" + is ChallengeNameType.CustomChallenge, + is ChallengeNameType.SelectMfaType, + is ChallengeNameType.SelectChallenge -> { + "ANSWER" + } is ChallengeNameType.SoftwareTokenMfa -> "SOFTWARE_TOKEN_MFA_CODE" - is ChallengeNameType.EmailOtp -> "EMAIL_OTP_CODE" // TOTP is not part of this because, it follows a completely different setup path is ChallengeNameType.MfaSetup -> { if (isMfaSetupSelectionChallenge(challenge)) { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt index 2a89b5c1f4..059f26a39e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt @@ -16,17 +16,22 @@ package com.amplifyframework.auth.cognito.actions import android.os.Build +import aws.sdk.kotlin.services.cognitoidentityprovider.initiateAuth +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmDeviceRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceSecretVerifierConfigType import com.amplifyframework.AmplifyException import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.CognitoDeviceHelper +import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper import com.amplifyframework.auth.exceptions.ServiceException import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.SignInActions import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.CredentialType import com.amplifyframework.statemachine.codegen.data.DeviceMetadata +import com.amplifyframework.statemachine.codegen.data.SignInMethod import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.CustomSignInEvent import com.amplifyframework.statemachine.codegen.events.DeviceSRPSignInEvent @@ -35,12 +40,24 @@ import com.amplifyframework.statemachine.codegen.events.SRPEvent 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.WebAuthnEvent internal object SignInCognitoActions : SignInActions { + private const val KEY_SECRET_HASH = "SECRET_HASH" + private const val KEY_USERNAME = "USERNAME" + override fun startSRPAuthAction(event: SignInEvent.EventType.InitiateSignInWithSRP) = Action("StartSRPAuth") { id, dispatcher -> logger.verbose("$id Starting execution") - val evt = SRPEvent(SRPEvent.EventType.InitiateSRP(event.username, event.password, event.metadata)) + val evt = SRPEvent( + SRPEvent.EventType.InitiateSRP( + username = event.username, + password = event.password, + metadata = event.metadata, + authFlowType = event.authFlowType, + respondToAuthChallenge = event.respondToAuthChallenge + ) + ) logger.verbose("$id Sending event ${evt.type}") dispatcher.send(evt) } @@ -59,7 +76,12 @@ internal object SignInCognitoActions : SignInActions { Action("StartMigrationAuth") { id, dispatcher -> logger.verbose("$id Starting execution") val evt = SignInEvent( - SignInEvent.EventType.InitiateMigrateAuth(event.username, event.password, event.metadata) + SignInEvent.EventType.InitiateMigrateAuth( + username = event.username, + password = event.password, + metadata = event.metadata, + authFlowType = event.authFlowType + ) ) logger.verbose("$id Sending event ${evt.type}") dispatcher.send(evt) @@ -151,4 +173,75 @@ internal object SignInCognitoActions : SignInActions { logger.verbose("$id Sending event ${evt.type}") dispatcher.send(evt) } + + override fun initiateWebAuthnSignInAction(event: SignInEvent.EventType.InitiateWebAuthnSignIn) = + Action("initiateWebAuthnSignIn") { id, dispatcher -> + logger.verbose("$id Starting excution") + val signInContext = event.signInContext + val requestJson = signInContext.requestJson + val evt = if (requestJson == null) { + // If we don't already have the request JSON then fetch it + WebAuthnEvent(WebAuthnEvent.EventType.FetchCredentialOptions(signInContext)) + } else { + // We do have the request JSON so go directly to asserting it + WebAuthnEvent(WebAuthnEvent.EventType.AssertCredentialOptions(signInContext)) + } + logger.verbose("$id sending event ${evt.type}") + dispatcher.send(evt) + } + + override fun autoSignInAction(event: SignInEvent.EventType.InitiateAutoSignIn): Action = + Action("AutoSignIn") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val username = event.signInData.username + val secretHash = AuthHelper.getSecretHash( + username, + configuration.userPool?.appClient, + configuration.userPool?.appClientSecret + ) + + val authParams = mutableMapOf( + KEY_USERNAME to username, + ) + secretHash?.let { authParams[KEY_SECRET_HASH] = it } + + val encodedContextData = getUserContextData(username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { + authFlow = AuthFlowType.UserAuth + clientId = configuration.userPool?.appClient + authParameters = authParams + clientMetadata = event.signInData.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + session = event.signInData.session + } + + if (response != null) { + SignInChallengeHelper.evaluateNextStep( + username = username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult, + signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + ) + } else { + throw ServiceException( + "Sign in failed", + AmplifyException.TODO_RECOVERY_SUGGESTION + ) + } + } catch (e: Exception) { + val signInError = SignInEvent(SignInEvent.EventType.ThrowError(e)) + logger.verbose("$id Sending event ${signInError.type}") + dispatcher.send(signInError) + + AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActions.kt new file mode 100644 index 0000000000..b4f787e55d --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActions.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2024 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.actions + +import aws.sdk.kotlin.services.cognitoidentityprovider.confirmSignUp +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AnalyticsMetadataType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType +import aws.sdk.kotlin.services.cognitoidentityprovider.signUp +import com.amplifyframework.auth.AuthCodeDeliveryDetails +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.AuthHelper +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.auth.result.step.AuthNextSignUpStep +import com.amplifyframework.auth.result.step.AuthSignUpStep +import com.amplifyframework.statemachine.Action +import com.amplifyframework.statemachine.codegen.actions.SignUpActions +import com.amplifyframework.statemachine.codegen.data.SignUpData +import com.amplifyframework.statemachine.codegen.events.SignUpEvent + +internal object SignUpCognitoActions : SignUpActions { + + override fun initiateSignUpAction(event: SignUpEvent.EventType.InitiateSignUp): Action = + Action("InitiatingSignUp") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val username = event.signUpData.username + val encodedContextData = getUserContextData(username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = cognitoAuthService.cognitoIdentityProviderClient?.signUp { + this.username = username + this.password = event.password + this.userAttributes = event.userAttributes?.map { + AttributeType { + name = it.key.keyString + value = it.value + } + } + this.clientId = configuration.userPool?.appClient + this.secretHash = AuthHelper.getSecretHash( + username, + configuration.userPool?.appClient, + configuration.userPool?.appClientSecret + ) + pinpointEndpointId?.let { + this.analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = it } + } + encodedContextData?.let { this.userContextData { encodedData = it } } + this.clientMetadata = event.signUpData.clientMetadata + this.validationData = event.signUpData.validationData?.mapNotNull { option -> + AttributeType { + name = option.key + value = option.value + } + } + } + + val codeDeliveryDetails = AuthCodeDeliveryDetails( + response?.codeDeliveryDetails?.destination ?: "", + AuthCodeDeliveryDetails.DeliveryMedium.fromString( + response?.codeDeliveryDetails?.deliveryMedium?.value + ), + response?.codeDeliveryDetails?.attributeName + ) + val signUpData = SignUpData( + username, + event.signUpData.validationData, + event.signUpData.clientMetadata, + response?.session, + response?.userSub + ) + if (response?.userConfirmed == true) { + var signUpStep = AuthSignUpStep.DONE + if (response.session != null) { + signUpStep = AuthSignUpStep.COMPLETE_AUTO_SIGN_IN + } + val signUpResult = + AuthSignUpResult( + true, + AuthNextSignUpStep( + signUpStep, + mapOf(), + codeDeliveryDetails + ), + response.userSub + ) + SignUpEvent(SignUpEvent.EventType.SignedUp(signUpData, signUpResult)) + } else { + val signUpResult = + AuthSignUpResult( + false, + AuthNextSignUpStep( + AuthSignUpStep.CONFIRM_SIGN_UP_STEP, + mapOf(), + codeDeliveryDetails + ), + response?.userSub + ) + SignUpEvent(SignUpEvent.EventType.InitiateSignUpComplete(signUpData, signUpResult)) + } + } catch (e: Exception) { + SignUpEvent(SignUpEvent.EventType.ThrowError(e)) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } + + override fun confirmSignUpAction(event: SignUpEvent.EventType.ConfirmSignUp): Action = + Action("ConfirmSignUp") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val username = event.signUpData.username + val encodedContextData = getUserContextData(username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = cognitoAuthService.cognitoIdentityProviderClient?.confirmSignUp { + this.username = username + this.confirmationCode = event.confirmationCode + this.clientId = configuration.userPool?.appClient + this.secretHash = AuthHelper.getSecretHash( + username, + configuration.userPool?.appClient, + configuration.userPool?.appClientSecret + ) + pinpointEndpointId?.let { + this.analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = it } + } + encodedContextData?.let { this.userContextData { encodedData = it } } + this.clientMetadata = event.signUpData.clientMetadata + this.session = event.signUpData.session + } + val signUpData = SignUpData( + username, + event.signUpData.validationData, + event.signUpData.clientMetadata, + response?.session, + event.signUpData.userId + ) + var signUpStep = AuthSignUpStep.DONE + if (response?.session != null) { + signUpStep = AuthSignUpStep.COMPLETE_AUTO_SIGN_IN + } + val signUpResult = + AuthSignUpResult( + true, + AuthNextSignUpStep( + signUpStep, + mapOf(), + null + ), + event.signUpData.userId + ) + SignUpEvent(SignUpEvent.EventType.SignedUp(signUpData, signUpResult)) + } catch (e: Exception) { + SignUpEvent(SignUpEvent.EventType.ThrowError(e)) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActions.kt new file mode 100644 index 0000000000..303f7dd8e4 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActions.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024 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.actions + +import aws.sdk.kotlin.services.cognitoidentityprovider.initiateAuth +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import com.amplifyframework.AmplifyException +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.AuthHelper +import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.exceptions.ServiceException +import com.amplifyframework.statemachine.Action +import com.amplifyframework.statemachine.codegen.actions.UserAuthSignInActions +import com.amplifyframework.statemachine.codegen.data.SignInMethod +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent + +internal object UserAuthSignInCognitoActions : UserAuthSignInActions { + private const val KEY_SECRET_HASH = "SECRET_HASH" + private const val KEY_USERNAME = "USERNAME" + private const val KEY_DEVICE_KEY = "DEVICE_KEY" + private const val KEY_USERID_FOR_SRP = "USER_ID_FOR_SRP" + private const val KEY_PREFERRED_CHALLENGE = "PREFERRED_CHALLENGE" + + override fun initiateUserAuthSignIn(event: SignInEvent.EventType.InitiateUserAuth): Action = + Action("InitUserAuth") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val secretHash = AuthHelper.getSecretHash( + event.username, + configuration.userPool?.appClient, + configuration.userPool?.appClientSecret + ) + + val authParams = mutableMapOf(KEY_USERNAME to event.username) + + secretHash?.let { authParams[KEY_SECRET_HASH] = it } + + event.preferredChallenge?.let { authParams[KEY_PREFERRED_CHALLENGE] = it.toString() } + + val encodedContextData = getUserContextData(event.username) + val deviceMetadata = getDeviceMetadata(event.username) + deviceMetadata?.let { authParams[KEY_DEVICE_KEY] = it.deviceKey } + val pinpointEndpointId = getPinpointEndpointId() + + val initiateAuthResponse = cognitoAuthService.cognitoIdentityProviderClient?.initiateAuth { + authFlow = AuthFlowType.UserAuth + clientId = configuration.userPool?.appClient + authParameters = authParams + clientMetadata = event.metadata + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + } + + val resolvedSession = initiateAuthResponse?.session + val resolvedChallenges = initiateAuthResponse?.availableChallenges + if (initiateAuthResponse?.challengeName == ChallengeNameType.SelectChallenge && + resolvedSession != null && + resolvedChallenges != null + ) { + val activeUserName = AuthHelper.getActiveUsername( + username = event.username, + alternateUsername = initiateAuthResponse.challengeParameters?.get(KEY_USERNAME), + userIDForSRP = initiateAuthResponse.challengeParameters?.get( + KEY_USERID_FOR_SRP + ) + ) + + val listOfChallenges = resolvedChallenges.map { it.value } + + SignInChallengeHelper.evaluateNextStep( + username = activeUserName, + challengeNameType = ChallengeNameType.SelectChallenge, + session = resolvedSession, + availableChallenges = listOfChallenges, + authenticationResult = initiateAuthResponse.authenticationResult, + callingActivity = event.callingActivity, + signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + ) + } else if (isSupportedChallenge(initiateAuthResponse?.challengeName) && + initiateAuthResponse?.challengeParameters != null && + resolvedSession != null + ) { + val activeUserName = AuthHelper.getActiveUsername( + username = event.username, + alternateUsername = initiateAuthResponse.challengeParameters?.get(KEY_USERNAME), + userIDForSRP = initiateAuthResponse.challengeParameters?.get( + KEY_USERID_FOR_SRP + ) + ) + + SignInChallengeHelper.evaluateNextStep( + username = activeUserName, + challengeNameType = initiateAuthResponse.challengeName, + session = resolvedSession, + challengeParameters = initiateAuthResponse.challengeParameters, + authenticationResult = initiateAuthResponse.authenticationResult, + callingActivity = event.callingActivity, + signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + ) + } else { + throw ServiceException("Sign in failed", AmplifyException.TODO_RECOVERY_SUGGESTION) + } + } catch (e: Exception) { + val errorEvent = SignInEvent(SignInEvent.EventType.ThrowError(e)) + logger.verbose("$id Sending event ${errorEvent.type}") + dispatcher.send(errorEvent) + + AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } + + private fun isSupportedChallenge(challengeName: ChallengeNameType?): Boolean = challengeName != null && + ( + challengeName is ChallengeNameType.EmailOtp || + challengeName is ChallengeNameType.SmsOtp || + challengeName is ChallengeNameType.WebAuthn + ) +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActions.kt new file mode 100644 index 0000000000..bad7c97800 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActions.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2024 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.actions + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.respondToAuthChallenge +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper +import com.amplifyframework.auth.cognito.requireIdentityProviderClient +import com.amplifyframework.statemachine.Action +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.actions.WebAuthnSignInActions +import com.amplifyframework.statemachine.codegen.data.SignInMethod +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.data.requireRequestJson +import com.amplifyframework.statemachine.codegen.data.requireResponseJson +import com.amplifyframework.statemachine.codegen.events.WebAuthnEvent + +internal object WebAuthnSignInCognitoActions : WebAuthnSignInActions { + private enum class ChallengeResponse(val key: String) { + USERNAME("USERNAME"), + CREDENTIAL("CREDENTIAL"), + ANSWER("ANSWER") + } + + override fun fetchCredentialOptions(event: WebAuthnEvent.EventType.FetchCredentialOptions): Action = + safeAction(event.signInContext) { + val signInContext = event.signInContext + val client = requireIdentityProviderClient() + val encodedContextData = getUserContextData(signInContext.username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = client.respondToAuthChallenge { + challengeName = ChallengeNameType.SelectChallenge + clientId = configuration.userPool?.appClient + challengeResponses = mapOf( + ChallengeResponse.USERNAME.key to signInContext.username, + ChallengeResponse.ANSWER.key to ChallengeNameType.WebAuthn.value + ) + session = signInContext.session + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + } + + SignInChallengeHelper.evaluateNextStep( + username = signInContext.username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult, + callingActivity = signInContext.callingActivity + ) + } + + override fun assertCredentials(event: WebAuthnEvent.EventType.AssertCredentialOptions): Action = + safeAction(event.signInContext) { + val helper = WebAuthnHelper(context) + val responseJson = helper.getCredential( + requestJson = event.signInContext.requireRequestJson(), + callingActivity = event.signInContext.callingActivity + ) + val newContext = event.signInContext.copy(responseJson = responseJson) + WebAuthnEvent(WebAuthnEvent.EventType.VerifyCredentialsAndSignIn(newContext)) + } + + override fun verifyCredentialAndSignIn(event: WebAuthnEvent.EventType.VerifyCredentialsAndSignIn): Action = + safeAction(event.signInContext) { + val signInContext = event.signInContext + val client = requireIdentityProviderClient() + val encodedContextData = getUserContextData(signInContext.username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = client.respondToAuthChallenge { + challengeName = ChallengeNameType.WebAuthn + clientId = configuration.userPool?.appClient + challengeResponses = mapOf( + ChallengeResponse.USERNAME.key to signInContext.username, + ChallengeResponse.CREDENTIAL.key to signInContext.requireResponseJson() + ) + session = signInContext.session + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { userContextData { encodedData = it } } + } + + SignInChallengeHelper.evaluateNextStep( + username = signInContext.username, + challengeNameType = ChallengeNameType.WebAuthn, + session = signInContext.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult, + callingActivity = signInContext.callingActivity, + signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + ) + } + + private fun safeAction(context: WebAuthnSignInContext, block: suspend AuthEnvironment.() -> StateMachineEvent) = + Action { _, dispatcher -> + val evt = try { + block() + } catch (e: Exception) { + WebAuthnEvent(WebAuthnEvent.EventType.ThrowError(e, context)) + } + dispatcher.send(evt) + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/UserCancelledException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/UserCancelledException.kt index b8251c8876..4649548864 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/UserCancelledException.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/UserCancelledException.kt @@ -20,6 +20,7 @@ import com.amplifyframework.auth.exceptions.ServiceException * Could not complete an action because it was cancelled by the user. * @param message An error message describing why this exception was thrown * @param recoverySuggestion Text suggesting a way to recover from the error being described + * @param cause The cause of the cancellation, if any */ -open class UserCancelledException(message: String, recoverySuggestion: String) : - ServiceException(message, recoverySuggestion) +open class UserCancelledException(message: String, recoverySuggestion: String, cause: Throwable? = null) : + ServiceException(message, recoverySuggestion, cause) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/WebAuthnNotEnabledException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/WebAuthnNotEnabledException.kt new file mode 100644 index 0000000000..e92816732a --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/WebAuthnNotEnabledException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.exceptions.service + +import com.amplifyframework.auth.exceptions.ServiceException + +/** + * Could not perform the action because WebAuthn is not enabled in the Cognito user pool + */ +class WebAuthnNotEnabledException internal constructor(cause: Throwable?) : + ServiceException( + message = "WebAuthn is not enabled for this userpool", + recoverySuggestion = "Ensure that your userpool is setup to have WebAuthn enabled", + cause = cause + ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnCredentialAlreadyExistsException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnCredentialAlreadyExistsException.kt new file mode 100644 index 0000000000..a8ee880815 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnCredentialAlreadyExistsException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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.exceptions.webauthn + +/** + * This exception occurs if associateWebAuthnCredential is invoked on a device that was already associated for the user + */ +class WebAuthnCredentialAlreadyExistsException internal constructor(cause: Throwable?) : + WebAuthnFailedException( + message = "The credential is already associated with this user", + recoverySuggestion = "Remove the old WebAuthn credential and try again", + cause = cause + ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException.kt new file mode 100644 index 0000000000..a56f90b3d0 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnFailedException.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.exceptions.webauthn + +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.exceptions.UnknownException + +/** + * A non-specific exception that indicates a failure interacting with Android's CredentialManager APIs + */ +open class WebAuthnFailedException internal constructor( + message: String, + recoverySuggestion: String? = null, + cause: Throwable? = null +) : AuthException( + message = message, + recoverySuggestion = recoverySuggestion + ?: if (cause == null) { + UnknownException.RECOVERY_SUGGESTION_WITHOUT_THROWABLE + } else { + UnknownException.RECOVERY_SUGGESTION_WITH_THROWABLE + }, + cause = cause +) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnNotSupportedException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnNotSupportedException.kt new file mode 100644 index 0000000000..a63c6c84c3 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnNotSupportedException.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.exceptions.webauthn + +/** + * Exception that is thrown because WebAuthn is not supported on the device. This indicates that either the device + * did not ship with WebAuthn support, or that your application is missing a required dependency or service. + */ +class WebAuthnNotSupportedException internal constructor(cause: Throwable?) : + WebAuthnFailedException( + message = "WebAuthn is not supported on this device", + recoverySuggestion = TODO_RECOVERY_SUGGESTION, + cause = cause + ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnRpMismatchException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnRpMismatchException.kt new file mode 100644 index 0000000000..17a9e132d9 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/webauthn/WebAuthnRpMismatchException.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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.exceptions.webauthn + +/** + * This Exception indicates that there is was a problem verifying your application against the configured relying party + * in your User Pool. This could be because your application has a different package or signing key than the ones + * specified in the deployed assetlinks.json file. For more details about this file please refer to the + * Android documentation here: https://developer.android.com/identity/sign-in/credential-manager#add-support-dal + */ +class WebAuthnRpMismatchException internal constructor(cause: Throwable?) : + WebAuthnFailedException( + message = "Unable to verify Relying Party data", + recoverySuggestion = + "Check that you have a valid assetlinks.json file deployed to your RP that specifies the " + + "correct package name, signing key fingerprints, and grants permission for " + + "delegate_permission/common.get_login_creds. See Android Credential Manager documentation for details.", + cause = cause + ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFactorTypeHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFactorTypeHelper.kt new file mode 100644 index 0000000000..18b35975c3 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFactorTypeHelper.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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.helpers + +import com.amplifyframework.auth.AuthFactorType + +internal fun String.toAuthFactorTypeOrNull() = AuthFactorType.entries.firstOrNull { it.challengeResponse == this } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFlowTypeHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFlowTypeHelper.kt new file mode 100644 index 0000000000..ae9f2e1889 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthFlowTypeHelper.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.helpers + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType as CognitoAuthFlowType +import com.amplifyframework.auth.cognito.options.AuthFlowType + +internal fun AuthFlowType.toCognitoType() = when (this) { + AuthFlowType.USER_SRP_AUTH -> CognitoAuthFlowType.UserSrpAuth + AuthFlowType.CUSTOM_AUTH -> CognitoAuthFlowType.CustomAuth + AuthFlowType.CUSTOM_AUTH_WITH_SRP -> CognitoAuthFlowType.CustomAuth + AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP -> CognitoAuthFlowType.CustomAuth + AuthFlowType.USER_PASSWORD_AUTH -> CognitoAuthFlowType.UserPasswordAuth + AuthFlowType.USER_AUTH -> CognitoAuthFlowType.UserAuth +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthLogger.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthLogger.kt new file mode 100644 index 0000000000..db9586e9e0 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/AuthLogger.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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. + */ + +@file:JvmName("AuthLogger") + +package com.amplifyframework.auth.cognito.helpers + +import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin.Companion.AWS_COGNITO_AUTH_LOG_NAMESPACE +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.category.CategoryType + +internal fun Any.authLogger() = + Amplify.Logging.logger(CategoryType.AUTH, AWS_COGNITO_AUTH_LOG_NAMESPACE.format(this::class.java.simpleName)) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/FlowExtensions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/FlowExtensions.kt new file mode 100644 index 0000000000..0d9d345425 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/FlowExtensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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.helpers + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.takeWhile + +internal suspend fun Flow.collectWhile(collector: (T) -> Boolean) { + this.takeWhile { + collector(it) + }.collect() +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt index 96511090b4..02d667a6c3 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt @@ -15,9 +15,12 @@ package com.amplifyframework.auth.cognito.helpers +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.ChallengeParameter +import com.amplifyframework.statemachine.codegen.data.challengeNameType @Throws(IllegalArgumentException::class) internal fun getMFAType(value: String) = when (value) { @@ -48,15 +51,15 @@ internal val MFAType.value: String } internal fun isMfaSetupSelectionChallenge(challenge: AuthChallenge) = - challenge.challengeName == "MFA_SETUP" && + challenge.challengeNameType == ChallengeNameType.MfaSetup && getAllowedMFASetupTypesFromChallengeParameters(challenge.parameters).size > 1 internal fun isEmailMfaSetupChallenge(challenge: AuthChallenge) = - challenge.challengeName == "MFA_SETUP" && + challenge.challengeNameType == ChallengeNameType.MfaSetup && getAllowedMFASetupTypesFromChallengeParameters(challenge.parameters) == setOf(MFAType.EMAIL) internal fun getAllowedMFATypesFromChallengeParameters(challengeParameters: Map?): Set { - val mfasCanChoose = challengeParameters?.get("MFAS_CAN_CHOOSE") ?: return emptySet() + val mfasCanChoose = challengeParameters?.get(ChallengeParameter.MfasCanChoose.key) ?: return emptySet() val result = mutableSetOf() mfasCanChoose.replace(Regex("\\[|\\]|\""), "").split(",").forEach { when (it) { @@ -71,7 +74,7 @@ internal fun getAllowedMFATypesFromChallengeParameters(challengeParameters: Map< // We exclude SMS as a setup type internal fun getAllowedMFASetupTypesFromChallengeParameters(challengeParameters: Map?): Set { - val mfasCanSetup = challengeParameters?.get("MFAS_CAN_SETUP") ?: return emptySet() + val mfasCanSetup = challengeParameters?.get(ChallengeParameter.MfasCanSetup.key) ?: return emptySet() val result = mutableSetOf() mfasCanSetup.replace(Regex("\\[|\\]|\""), "").split(",").forEach { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt index feb2481419..1e22f4b02d 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt @@ -15,12 +15,14 @@ package com.amplifyframework.auth.cognito.helpers +import android.app.Activity import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.smithy.kotlin.runtime.time.Instant import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails.DeliveryMedium import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.exceptions.UnknownException @@ -30,13 +32,17 @@ import com.amplifyframework.auth.result.step.AuthSignInStep import com.amplifyframework.core.Consumer import com.amplifyframework.statemachine.StateMachineEvent import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.ChallengeParameter 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.SignInTOTPSetupData import com.amplifyframework.statemachine.codegen.data.SignedInData +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.data.challengeNameType import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent +import java.lang.ref.WeakReference import java.util.Date import kotlin.time.Duration.Companion.seconds @@ -45,73 +51,97 @@ internal object SignInChallengeHelper { username: String, challengeNameType: ChallengeNameType?, session: String?, - challengeParameters: Map?, + challengeParameters: Map? = null, + availableChallenges: List? = null, authenticationResult: AuthenticationResultType?, + callingActivity: WeakReference = WeakReference(null), signInMethod: SignInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_SRP_AUTH) - ): StateMachineEvent { - return when { - authenticationResult != null -> { - authenticationResult.let { - val userId = it.accessToken?.let { token -> SessionHelper.getUserSub(token) } ?: "" - val expiresIn = Instant.now().plus(it.expiresIn.seconds).epochSeconds - val tokens = CognitoUserPoolTokens(it.idToken, it.accessToken, it.refreshToken, expiresIn) - val signedInData = SignedInData( - userId, - username, - Date(), - signInMethod, - tokens - ) - it.newDeviceMetadata?.let { metadata -> - SignInEvent( - SignInEvent.EventType.ConfirmDevice( - DeviceMetadata.Metadata( - metadata.deviceKey ?: "", - metadata.deviceGroupKey ?: "" - ), - signedInData - ) - ) - } ?: AuthenticationEvent( - AuthenticationEvent.EventType.SignInCompleted( - signedInData, - DeviceMetadata.Empty + ): StateMachineEvent = when { + authenticationResult != null -> { + authenticationResult.let { + val userId = it.accessToken?.let { token -> SessionHelper.getUserSub(token) } ?: "" + val expiresIn = Instant.now().plus(it.expiresIn.seconds).epochSeconds + val tokens = CognitoUserPoolTokens(it.idToken, it.accessToken, it.refreshToken, expiresIn) + val signedInData = SignedInData( + userId, + username, + Date(), + signInMethod, + tokens + ) + it.newDeviceMetadata?.let { metadata -> + SignInEvent( + SignInEvent.EventType.ConfirmDevice( + DeviceMetadata.Metadata( + metadata.deviceKey ?: "", + metadata.deviceGroupKey ?: "" + ), + signedInData ) ) - } - } - challengeNameType is ChallengeNameType.SmsMfa || - challengeNameType is ChallengeNameType.CustomChallenge || - challengeNameType is ChallengeNameType.NewPasswordRequired || - challengeNameType is ChallengeNameType.SoftwareTokenMfa || - challengeNameType is ChallengeNameType.EmailOtp || - challengeNameType is ChallengeNameType.SelectMfaType -> { - val challenge = - AuthChallenge(challengeNameType.value, username, session, challengeParameters) - SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) + } ?: AuthenticationEvent( + AuthenticationEvent.EventType.SignInCompleted( + signedInData, + DeviceMetadata.Empty + ) + ) } - challengeNameType is ChallengeNameType.MfaSetup -> { - val allowedMFASetupTypes = getAllowedMFASetupTypesFromChallengeParameters(challengeParameters) - val challenge = AuthChallenge(challengeNameType.value, username, session, challengeParameters) + } + challengeNameType is ChallengeNameType.SmsMfa || + challengeNameType is ChallengeNameType.CustomChallenge || + challengeNameType is ChallengeNameType.NewPasswordRequired || + challengeNameType is ChallengeNameType.SoftwareTokenMfa || + challengeNameType is ChallengeNameType.SelectMfaType || + challengeNameType is ChallengeNameType.SmsOtp || + challengeNameType is ChallengeNameType.EmailOtp -> { + val challenge = + AuthChallenge(challengeNameType.value, username, session, challengeParameters) + SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) + } + challengeNameType is ChallengeNameType.MfaSetup -> { + val allowedMFASetupTypes = getAllowedMFASetupTypesFromChallengeParameters(challengeParameters) + val challenge = AuthChallenge(challengeNameType.value, username, session, challengeParameters) - if (allowedMFASetupTypes.contains(MFAType.EMAIL)) { - SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) - } else if (allowedMFASetupTypes.contains(MFAType.TOTP)) { - val setupTOTPData = SignInTOTPSetupData("", session, username) - SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData, challenge.parameters)) - } else { - SignInEvent( - SignInEvent.EventType.ThrowError( - Exception("Cannot initiate MFA setup from available Types: $allowedMFASetupTypes") - ) + if (allowedMFASetupTypes.contains(MFAType.EMAIL)) { + SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) + } else if (allowedMFASetupTypes.contains(MFAType.TOTP)) { + val setupTOTPData = SignInTOTPSetupData("", session, username) + SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData, challenge.parameters)) + } else { + SignInEvent( + SignInEvent.EventType.ThrowError( + Exception("Cannot initiate MFA setup from available Types: $allowedMFASetupTypes") ) - } - } - challengeNameType is ChallengeNameType.DeviceSrpAuth -> { - SignInEvent(SignInEvent.EventType.InitiateSignInWithDeviceSRP(username, mapOf())) + ) } - else -> SignInEvent(SignInEvent.EventType.ThrowError(Exception("Response did not contain sign in info."))) } + challengeNameType is ChallengeNameType.DeviceSrpAuth -> { + SignInEvent(SignInEvent.EventType.InitiateSignInWithDeviceSRP(username, mapOf())) + } + challengeNameType is ChallengeNameType.SelectChallenge -> { + SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = username, + session = session, + parameters = null, + availableChallenges = availableChallenges + ) + ) + ) + } + challengeNameType is ChallengeNameType.WebAuthn -> { + val requestOptions = challengeParameters?.get(ChallengeParameter.CredentialRequestOptions.key) + val signInContext = WebAuthnSignInContext( + username = username, + callingActivity = callingActivity, + session = session, + requestJson = requestOptions + ) + SignInEvent(SignInEvent.EventType.InitiateWebAuthnSignIn(signInContext)) + } + else -> SignInEvent(SignInEvent.EventType.ThrowError(Exception("Response did not contain sign in info."))) } fun getNextStep( @@ -121,23 +151,29 @@ internal object SignInChallengeHelper { signInTOTPSetupData: SignInTOTPSetupData? = null, allowedMFAType: Set? = null ) { - val challengeParams = challenge.parameters?.toMutableMap() ?: mapOf() + val challengeParams = challenge.parameters ?: emptyMap() - when (ChallengeNameType.fromValue(challenge.challengeName)) { - is ChallengeNameType.SmsMfa -> { + when (challenge.challengeNameType) { + is ChallengeNameType.SmsMfa, + ChallengeNameType.EmailOtp, + ChallengeNameType.SmsOtp -> { val deliveryDetails = AuthCodeDeliveryDetails( - challengeParams.getValue("CODE_DELIVERY_DESTINATION"), - DeliveryMedium.fromString( - challengeParams.getValue("CODE_DELIVERY_DELIVERY_MEDIUM") - ) + challengeParams.getValue(ChallengeParameter.CodeDeliveryDestination.key), + DeliveryMedium.fromString(challengeParams.getValue(ChallengeParameter.CodeDeliveryMedium.key)) ) + val signInStep = if (challenge.challengeNameType == ChallengeNameType.SmsMfa) { + AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE + } else { + AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP + } val authSignInResult = AuthSignInResult( false, AuthNextSignInStep( - AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, + signInStep, mapOf(), deliveryDetails, null, + null, null ) ) @@ -151,6 +187,7 @@ internal object SignInChallengeHelper { challengeParams, null, null, + null, null ) ) @@ -164,6 +201,7 @@ internal object SignInChallengeHelper { challengeParams, null, null, + null, null ) ) @@ -177,6 +215,7 @@ internal object SignInChallengeHelper { emptyMap(), null, null, + null, null ) ) @@ -193,7 +232,8 @@ internal object SignInChallengeHelper { emptyMap(), null, null, - allowedMFASetupTypes + allowedMFASetupTypes, + null ) ) onSuccess.accept(authSignInResult) @@ -205,7 +245,8 @@ internal object SignInChallengeHelper { challengeParams, null, TOTPSetupDetails(signInTOTPSetupData.secretCode, signInTOTPSetupData.username), - allowedMFAType + allowedMFAType, + null ) ) onSuccess.accept(authSignInResult) @@ -217,7 +258,8 @@ internal object SignInChallengeHelper { emptyMap(), null, null, - allowedMFAType + allowedMFAType, + null ) ) onSuccess.accept(authSignInResult) @@ -233,30 +275,22 @@ internal object SignInChallengeHelper { mapOf(), null, null, - getAllowedMFATypesFromChallengeParameters(challengeParams) + getAllowedMFATypesFromChallengeParameters(challengeParams), + null ) ) onSuccess.accept(authSignInResult) } - is ChallengeNameType.EmailOtp -> { - val codeDeliveryMedium = DeliveryMedium.fromString( - challengeParams["CODE_DELIVERY_DELIVERY_MEDIUM"] ?: DeliveryMedium.UNKNOWN.value - ) - val codeDeliveryDestination = challengeParams["CODE_DELIVERY_DESTINATION"] - val deliveryDetails = if (codeDeliveryDestination != null) { - AuthCodeDeliveryDetails(codeDeliveryDestination, codeDeliveryMedium) - } else { - null - } - + is ChallengeNameType.SelectChallenge -> { val authSignInResult = AuthSignInResult( false, AuthNextSignInStep( - AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, + AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, mapOf(), - deliveryDetails, null, - null + null, + null, + getAvailableFactors(challenge.availableChallenges) ) ) onSuccess.accept(authSignInResult) @@ -264,4 +298,20 @@ internal object SignInChallengeHelper { else -> onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) } } + + private fun getAvailableFactors(possibleFactors: List?): Set { + val result = mutableSetOf() + if (possibleFactors == null) { + throw UnknownException(cause = Exception("Tried to parse available factors but found none.")) + } else { + possibleFactors.forEach { + try { + result.add(AuthFactorType.valueOf(it)) + } catch (exception: IllegalArgumentException) { + throw UnknownException(cause = Exception("Tried to parse an unrecognized AuthFactorType")) + } + } + } + return result + } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelper.kt new file mode 100644 index 0000000000..daae67eeb5 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelper.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2024 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.helpers + +import android.app.Activity +import android.content.Context +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.CreateCredentialCancellationException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException +import androidx.credentials.exceptions.CreateCredentialUnsupportedException +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.GetCredentialProviderConfigurationException +import androidx.credentials.exceptions.GetCredentialUnsupportedException +import androidx.credentials.exceptions.domerrors.DataError +import androidx.credentials.exceptions.domerrors.InvalidStateError +import androidx.credentials.exceptions.domerrors.NotAllowedError +import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException +import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException +import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException +import com.amplifyframework.AmplifyException +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException +import com.amplifyframework.auth.cognito.exceptions.webauthn.WebAuthnCredentialAlreadyExistsException +import com.amplifyframework.auth.cognito.exceptions.webauthn.WebAuthnFailedException +import com.amplifyframework.auth.cognito.exceptions.webauthn.WebAuthnNotSupportedException +import com.amplifyframework.auth.cognito.exceptions.webauthn.WebAuthnRpMismatchException +import java.lang.ref.WeakReference + +internal class WebAuthnHelper( + private val context: Context, + private val credentialManager: CredentialManager = CredentialManager.create(context) +) { + + private val logger = authLogger() + + suspend fun getCredential(requestJson: String, callingActivity: WeakReference): String { + try { + // Construct the request for CredentialManager. We're only interested in PublicKey credentials + val options = GetPublicKeyCredentialOption(requestJson = requestJson) + val request = GetCredentialRequest(credentialOptions = listOf(options)) + + logger.verbose("Prompting user for PassKey authorization") + val result = credentialManager.getCredential(context = callingActivity.resolveContext(), request = request) + + // Extract the Public Key credential response. This is what we send to Cognito. + val publicKeyResult = result.credential as? PublicKeyCredential ?: throw WebAuthnFailedException( + "Android returned wrong credential type", + AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION + ) + return publicKeyResult.authenticationResponseJson + } catch (e: GetCredentialException) { + throw e.toAuthException() + } + } + + suspend fun createCredential(requestJson: String, callingActivity: Activity): String { + try { + // Create the request for CredentialManager + val request = CreatePublicKeyCredentialRequest(requestJson) + + // Create the credential + logger.verbose("Prompting user to create a PassKey") + val result: CreateCredentialResponse = credentialManager.createCredential(callingActivity, request) + + // Extract the Public Key registration response. This is what we send to Cognito. + val publicKeyResult = result as? CreatePublicKeyCredentialResponse ?: throw WebAuthnFailedException( + "Android created wrong credential type", + AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION + ) + return publicKeyResult.registrationResponseJson + } catch (e: CreateCredentialException) { + throw e.toAuthException() + } + } + + private fun WeakReference.resolveContext(): Context { + // Use the Activity context if provided. The Activity context will allow the authorization UI to be shown + // in the same Task instance - if we use the Application context instead it will launch a new Task. + // Customers should always provide a calling activity when using WebAuthn for the best user experience. + val activity = get() + if (activity == null) { + logger.warn( + "No Activity context available when accessing device PassKey. This will result in the system " + + "UI appearing in a new Task. We recommend setting the callingActivity option when invoking " + + "Amplify Auth APIs if you are using WebAuthn." + ) + } + return activity ?: context + } + + private fun CreateCredentialException.toAuthException(): AuthException = when (this) { + is CreateCredentialCancellationException -> userCancelledException() + is CreateCredentialProviderConfigurationException -> notSupported() + is CreateCredentialUnsupportedException -> notSupported() + is CreatePublicKeyCredentialDomException -> { + when (this.domError) { + is NotAllowedError -> userCancelledException() + is InvalidStateError -> alreadyExists() + is DataError -> rpMismatch() + else -> unknownException() + } + } + else -> unknownException() + } + + private fun GetCredentialException.toAuthException(): AuthException = when (this) { + is GetCredentialCancellationException -> userCancelledException() + is GetCredentialProviderConfigurationException -> notSupported() + is GetCredentialUnsupportedException -> notSupported() + is GetPublicKeyCredentialDomException -> { + when (this.domError) { + is NotAllowedError -> userCancelledException() + is DataError -> rpMismatch() + else -> unknownException() + } + } + else -> unknownException() + } + + // The exception returned when user cancels + private fun Exception.userCancelledException() = UserCancelledException( + message = "User cancelled granting access to PassKey", + recoverySuggestion = "Re-show the previous UI and allow user to try again", + cause = this + ).also { logger.verbose("User cancelled the PassKey authorization UI") } + + private fun CreatePublicKeyCredentialException.alreadyExists() = WebAuthnCredentialAlreadyExistsException(this) + private fun Exception.notSupported() = WebAuthnNotSupportedException(this) + private fun Exception.rpMismatch() = WebAuthnRpMismatchException(this) + + // The default exception returned when fetching credentials + private fun CreateCredentialException.unknownException() = + WebAuthnFailedException("Unable to create the passkey using the Androidx CredentialManager", cause = this) + private fun GetCredentialException.unknownException() = + WebAuthnFailedException("Unable to retrieve the passkey from the Androidx CredentialManager", cause = this) +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt index 08cf7fc78d..30b9417a67 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt @@ -15,8 +15,10 @@ package com.amplifyframework.auth.cognito.options +import android.app.Activity import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.options.AuthConfirmSignInOptions +import java.lang.ref.WeakReference /** * Cognito extension of confirm sign in options to add the platform specific fields. @@ -36,7 +38,12 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( * Get the friendly device name used to setup TOTP. * @return friendly device name */ - val friendlyDeviceName: String? + val friendlyDeviceName: String?, + /** + * Get the Activity instance, if any. + * @return A WeakReference to the Activity + */ + val callingActivity: WeakReference ) : AuthConfirmSignInOptions() { companion object { @@ -45,9 +52,7 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( * @return a builder object. */ @JvmStatic - fun builder(): CognitoBuilder { - return CognitoBuilder() - } + fun builder(): CognitoBuilder = CognitoBuilder() inline operator fun invoke(block: CognitoBuilder.() -> Unit) = CognitoBuilder().apply(block).build() } @@ -59,14 +64,13 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( private var metadata: Map = mapOf() private var userAttributes: List = listOf() private var friendlyDeviceName: String? = null + private var callingActivity: WeakReference = WeakReference(null) /** * Returns the type of builder this is to support proper flow with it being an extended class. * @return the type of builder this is to support proper flow with it being an extended class. */ - override fun getThis(): CognitoBuilder { - return this - } + override fun getThis(): CognitoBuilder = this /** * Set the metadata field for the object being built. @@ -90,10 +94,21 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( */ fun friendlyDeviceName(friendlyDeviceName: String) = apply { this.friendlyDeviceName = friendlyDeviceName } + /** + * Set the callingActivity field for the object being built. This should be set when using WebAuthn to ensure + * the optimal user experience. + * @param callingActivity The current Activity. + * @return the instance of the builder. + */ + fun callingActivity(callingActivity: Activity) = apply { + this.callingActivity = WeakReference(callingActivity) + } + /** * Construct and return the object with the values set in the builder. * @return a new instance of AWSCognitoAuthConfirmSignInOptions with the values specified in the builder. */ - override fun build() = AWSCognitoAuthConfirmSignInOptions(metadata, userAttributes, friendlyDeviceName) + override fun build() = + AWSCognitoAuthConfirmSignInOptions(metadata, userAttributes, friendlyDeviceName, callingActivity) } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions.kt new file mode 100644 index 0000000000..654557d3fe --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthListWebAuthnCredentialsOptions.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024 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.options + +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult + +/** + * Options for the listWebAuthnCredentials API that are specific to Cognito. + * @param nextToken The token returned the [AuthListWebAuthnCredentialsResult] that will load the next page of results. + * Should be null to load the first page. + * @param maxResults The maximum number of results to return per page. Set to null to use the default max. + */ +data class AWSCognitoAuthListWebAuthnCredentialsOptions internal constructor( + val nextToken: String?, + val maxResults: Int? +) : AuthListWebAuthnCredentialsOptions() { + companion object { + /** + * Create a [Builder] for this class + */ + @JvmStatic + fun builder() = Builder() + + /** + * Construct using a DSL syntax + */ + @JvmSynthetic + inline operator fun invoke(func: Builder.() -> Unit) = Builder().apply(func).build() + + /** + * Return the default options + */ + @JvmStatic + fun defaults() = builder().build() + + private fun AuthListWebAuthnCredentialsOptions.asCognitoOptions() = + this as? AWSCognitoAuthListWebAuthnCredentialsOptions + internal val AuthListWebAuthnCredentialsOptions.nextToken: String? + get() = this.asCognitoOptions()?.nextToken + internal val AuthListWebAuthnCredentialsOptions.maxResults: Int? + get() = this.asCognitoOptions()?.maxResults + } + + /** + * Builder for cognito-specific [AuthListWebAuthnCredentialsOptions]. + */ + class Builder : AuthListWebAuthnCredentialsOptions.Builder() { + /** + *The next token that was returned in the prior page of results. Set to null to load the first page. + */ + var nextToken: String? = null + @JvmSynthetic set + + /** + * The maximum number of results to return. Set to null to use the service-default max value. + */ + var maxResults: Int? = null + @JvmSynthetic set + + /** + * Returns this instance for typesafe chaining from the parent class + */ + override fun getThis() = this + + /** + * Set the next token to load a further page of results + * @param nextToken The next token that was returned in the prior page of results. Set to null to load the + * first page. + * @return This instance for chaining calls + */ + fun nextToken(nextToken: String?) = apply { this.nextToken = nextToken } + + /** + * Set the maximum number of results to return per page + * @param maxResults The maximum number of results to return. Set to null to use the service-default max + * value. + * @return This instance for chaining calls + */ + fun maxResults(maxResults: Int?) = apply { this.maxResults = maxResults } + + /** + * Builds the options object + * @return The constructed [AWSCognitoAuthListWebAuthnCredentialsOptions] objects + */ + override fun build() = AWSCognitoAuthListWebAuthnCredentialsOptions( + nextToken = nextToken, + maxResults = maxResults + ) + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions.java index a10a1b7183..91b8c3777f 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthSignInOptions.java @@ -15,13 +15,16 @@ package com.amplifyframework.auth.cognito.options; +import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; +import com.amplifyframework.auth.AuthFactorType; import com.amplifyframework.auth.options.AuthSignInOptions; import com.amplifyframework.util.Immutable; +import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -32,19 +35,30 @@ public final class AWSCognitoAuthSignInOptions extends AuthSignInOptions { private final Map metadata; private final AuthFlowType authFlowType; + private final AuthFactorType preferredFirstFactor; + private final WeakReference callingActivity; /** * Advanced options for signing in. * * @param metadata Additional custom attributes to be sent to the service such as information about the client * @param authFlowType AuthFlowType to be used by signIn API + * @param preferredFirstFactor The preferred authentication factor to use, if available. + * This is only used if authFlowType is USER_AUTH. + * @param callingActivity The Activity reference to use when showing the PassKey UI. + * This is only used if authFlowType is USER_AUTH and WebAuthn is + * used to sign in. */ protected AWSCognitoAuthSignInOptions( @NonNull Map metadata, - AuthFlowType authFlowType + AuthFlowType authFlowType, + AuthFactorType preferredFirstFactor, + WeakReference callingActivity ) { this.metadata = metadata; this.authFlowType = authFlowType; + this.preferredFirstFactor = preferredFirstFactor; + this.callingActivity = callingActivity; } /** @@ -67,6 +81,28 @@ public AuthFlowType getAuthFlowType() { return authFlowType; } + /** + * Get the preferred {@link AuthFactorType} to use when signing in with USER_AUTH. If that + * AuthFactorType is available for the user signing in then it will be used to authenticate + * the user, otherwise another factor may be used or the user may be prompted to select a + * factor. + * @return The preferred {@link AuthFactorType} to use, if available. + */ + @Nullable + public AuthFactorType getPreferredFirstFactor() { + return preferredFirstFactor; + } + + /** + * Get the Activity reference to use when showing the PassKey UI. This is only used if + * authFlowType is USER_AUTH and WebAuthn is used to sign in. + * @return The Activity reference + */ + @NonNull + public WeakReference getCallingActivity() { + return callingActivity; + } + /** * Get a builder object. * @@ -79,7 +115,7 @@ public static CognitoBuilder builder() { @Override public int hashCode() { - return ObjectsCompat.hash(getMetadata(), getAuthFlowType()); + return ObjectsCompat.hash(getMetadata(), getAuthFlowType(), getPreferredFirstFactor(), getCallingActivity()); } @Override @@ -91,7 +127,10 @@ public boolean equals(Object obj) { } else { AWSCognitoAuthSignInOptions authSignInOptions = (AWSCognitoAuthSignInOptions) obj; return ObjectsCompat.equals(getMetadata(), authSignInOptions.getMetadata()) && - ObjectsCompat.equals(getAuthFlowType(), authSignInOptions.getAuthFlowType()); + ObjectsCompat.equals(getAuthFlowType(), authSignInOptions.getAuthFlowType()) && + ObjectsCompat.equals(getPreferredFirstFactor(), + authSignInOptions.getPreferredFirstFactor()) && + ObjectsCompat.equals(getCallingActivity(), authSignInOptions.getCallingActivity()); } } @@ -100,6 +139,8 @@ public String toString() { return "AWSCognitoAuthSignInOptions{" + "metadata=" + getMetadata() + ", authFlowType=" + getAuthFlowType() + + ", preferredFirstFactor=" + getPreferredFirstFactor() + + ", callingActivity=" + getCallingActivity() + '}'; } @@ -109,6 +150,8 @@ public String toString() { public static final class CognitoBuilder extends Builder { private final Map metadata; private AuthFlowType authFlowType; + private AuthFactorType preferredFirstFactor; + private WeakReference callingActivity = new WeakReference<>(null); /** * Constructor for the builder. @@ -154,6 +197,35 @@ public CognitoBuilder authFlowType(@NonNull AuthFlowType authFlowType) { return getThis(); } + /** + * Set the preferred {@link AuthFactorType} to use when signing in with USER_AUTH. If that + * AuthFactorType is available for the user signing in then it will be used to authenticate + * the user. If this option is not set or is not available for the user then another factor + * may be used or the user may be prompted to select a factor. + * @param factorType The preferred factor. + * @return The builder object to continue building. + */ + @NonNull + public CognitoBuilder preferredFirstFactor(@Nullable AuthFactorType factorType) { + this.preferredFirstFactor = factorType; + return getThis(); + } + + /** + * Set the Activity reference to use when showing the PassKey UI. This is only used if + * authFlowType is USER_AUTH and WebAuthn is used to sign in. This option should always be + * set if your app may be expecting to use WebAuthn, as not setting this option will lead + * to a sub-optimal user experience when authenticating via WebAuthn. + * @param callingActivity The Activity instance. This is stored in a WeakReference so that + * it will not be leaked. + * @return The builder object to continue building. + */ + @NonNull + public CognitoBuilder callingActivity(@NonNull Activity callingActivity) { + this.callingActivity = new WeakReference<>(callingActivity); + return getThis(); + } + /** * Construct and return the object with the values set in the builder. * @@ -161,7 +233,12 @@ public CognitoBuilder authFlowType(@NonNull AuthFlowType authFlowType) { */ @NonNull public AWSCognitoAuthSignInOptions build() { - return new AWSCognitoAuthSignInOptions(Immutable.of(metadata), authFlowType); + return new AWSCognitoAuthSignInOptions( + Immutable.of(metadata), + authFlowType, + preferredFirstFactor, + callingActivity + ); } } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AuthFlowType.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AuthFlowType.java index 01dedd1a7b..796a3d4109 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AuthFlowType.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AuthFlowType.java @@ -44,7 +44,13 @@ public enum AuthFlowType { /** * type for USER_PASSWORD_AUTH. */ - USER_PASSWORD_AUTH("USER_PASSWORD_AUTH"); + USER_PASSWORD_AUTH("USER_PASSWORD_AUTH"), + + /** + * type for USER_AUTH. + */ + USER_AUTH("USER_AUTH"); + private String value; AuthFlowType(String value) { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult.kt new file mode 100644 index 0000000000..a602e45981 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/result/AWSCognitoAuthListWebAuthnCredentialsResult.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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.result + +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult +import com.amplifyframework.auth.result.AuthWebAuthnCredential +import java.time.Instant + +/** + * The cognito-specific result to the listWebAuthnCredentials API. + * @param credentials The returned credentials + * @param nextToken If there are multiple pages of results this will be non-null, and can be passed in the + * options object to fetch the next page. + */ +data class AWSCognitoAuthListWebAuthnCredentialsResult( + override val credentials: List, + val nextToken: String? +) : AuthListWebAuthnCredentialsResult + +internal data class CognitoWebAuthnCredential( + override val credentialId: String, + override val friendlyName: String?, + override val relyingPartyId: String, + override val createdAt: Instant +) : AuthWebAuthnCredential diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt new file mode 100644 index 0000000000..fbcc8f1efc --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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 android.app.Activity +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.completeWebAuthnRegistration +import aws.sdk.kotlin.services.cognitoidentityprovider.startWebAuthnRegistration +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper +import com.amplifyframework.auth.cognito.helpers.authLogger +import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn +import com.amplifyframework.statemachine.util.mask +import com.amplifyframework.util.JsonDocument +import com.amplifyframework.util.toJsonString + +internal class AssociateWebAuthnCredentialUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine, + private val webAuthnHelper: WebAuthnHelper +) { + private val logger = authLogger() + + @Suppress("UNUSED_PARAMETER") + suspend fun execute(callingActivity: Activity, options: AuthAssociateWebAuthnCredentialsOptions) { + // User must be signed in to call this API + stateMachine.requireAuthenticationState() + + val accessToken = fetchAuthSession.execute().accessToken + + // Step 1: Get the credential request JSON from Cognito + val requestJson = getCredentialRequestJson(accessToken) + logger.debug("Received credential request: ${requestJson.mask()}") + + // Step 2: Create the credential with Android and get the response JSON + val responseJson = webAuthnHelper.createCredential(requestJson, callingActivity) + logger.debug("Sending credential response: ${responseJson.mask()}") + + // Step 3: Send the response JSON back to Cognito to complete the registration + associateCredential(responseJson, accessToken) + } + + private suspend fun getCredentialRequestJson(accessToken: String?): String { + val response = client.startWebAuthnRegistration { + this.accessToken = accessToken + } + return response.credentialCreationOptions!!.toJsonString() + } + + private suspend fun associateCredential(credentialResponseJson: String, accessToken: String?) { + client.completeWebAuthnRegistration { + this.credential = JsonDocument(credentialResponseJson) + this.accessToken = accessToken + } + } +} 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 new file mode 100644 index 0000000000..2eec0b7585 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.RealAWSCognitoAuthPlugin +import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper +import com.amplifyframework.auth.cognito.requireIdentityProviderClient + +internal class AuthUseCaseFactory( + private val plugin: RealAWSCognitoAuthPlugin, + private val authEnvironment: AuthEnvironment, + private val stateMachine: AuthStateMachine +) { + + fun fetchAuthSession() = FetchAuthSessionUseCase(plugin) + + fun associateWebAuthnCredential() = AssociateWebAuthnCredentialUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine, + webAuthnHelper = WebAuthnHelper(authEnvironment.context) + ) + + fun listWebAuthnCredentials() = ListWebAuthnCredentialsUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine + ) + + fun deleteWebAuthnCredential() = DeleteWebAuthnCredentialUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine + ) +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt new file mode 100644 index 0000000000..97f898bdf5 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.deleteWebAuthnCredential +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn + +internal class DeleteWebAuthnCredentialUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine +) { + @Suppress("UNUSED_PARAMETER") + suspend fun execute(credentialId: String, options: AuthDeleteWebAuthnCredentialOptions) { + // User must be signed in to call this API + stateMachine.requireAuthenticationState() + + val accessToken = fetchAuthSession.execute().accessToken + client.deleteWebAuthnCredential { + this.accessToken = accessToken + this.credentialId = credentialId + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchAuthSessionUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchAuthSessionUseCase.kt new file mode 100644 index 0000000000..1eb9d96faa --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchAuthSessionUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.cognito.AWSCognitoAuthSession +import com.amplifyframework.auth.cognito.RealAWSCognitoAuthPlugin +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +internal class FetchAuthSessionUseCase( + private val plugin: RealAWSCognitoAuthPlugin +) { + suspend fun execute(): AWSCognitoAuthSession { + // TODO - we should migrate the fetch auth session business logic to this class + val session = suspendCoroutine { continuation -> + plugin.fetchAuthSession( + onSuccess = { continuation.resume(it) }, + onError = { continuation.resumeWithException(it) } + ) + } + return session as AWSCognitoAuthSession + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt new file mode 100644 index 0000000000..c5dea835e3 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 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.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.listWebAuthnCredentials +import aws.smithy.kotlin.runtime.time.toJvmInstant +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions.Companion.maxResults +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions.Companion.nextToken +import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.cognito.result.AWSCognitoAuthListWebAuthnCredentialsResult +import com.amplifyframework.auth.cognito.result.CognitoWebAuthnCredential +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn + +internal class ListWebAuthnCredentialsUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine +) { + suspend fun execute(options: AuthListWebAuthnCredentialsOptions): AWSCognitoAuthListWebAuthnCredentialsResult { + // User must be SignedIn to call this API + stateMachine.requireAuthenticationState() + + val token = fetchAuthSession.execute().accessToken + + val response = client.listWebAuthnCredentials { + accessToken = token + nextToken = options.nextToken + maxResults = options.maxResults + } + + val credentials = response.credentials.map { credential -> + CognitoWebAuthnCredential( + credentialId = credential.credentialId, + friendlyName = credential.friendlyCredentialName, + relyingPartyId = credential.relyingPartyId, + createdAt = credential.createdAt.toJvmInstant() + ) + } + + return AWSCognitoAuthListWebAuthnCredentialsResult( + credentials = credentials, + nextToken = response.nextToken + ) + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/StateMachine.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/StateMachine.kt index 42b4a9a169..5372a6bd83 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/StateMachine.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/StateMachine.kt @@ -17,12 +17,16 @@ package com.amplifyframework.statemachine import java.util.UUID import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.withContext internal typealias OnSubscribedCallback = () -> Unit @@ -49,43 +53,34 @@ internal class StateChangeListenerToken private constructor(val uuid: UUID) { internal open class StateMachine( resolver: StateMachineResolver, val environment: EnvironmentType, - executor: EffectExecutor? = null, - concurrentQueue: CoroutineDispatcher? = null, + private val dispatcherQueue: CoroutineDispatcher = Dispatchers.Default, + private val executor: EffectExecutor = ConcurrentEffectExecutor(dispatcherQueue), initialState: StateType? = null ) : EventDispatcher { private val resolver = resolver.eraseToAnyResolver() - private val executor: EffectExecutor - private var currentState = initialState ?: resolver.defaultState - private val dispatcherQueue: CoroutineDispatcher + // The current state of the state machine. Consumers can collect or read the current state from the read-only StateFlow + private val _state = MutableStateFlow(initialState ?: resolver.defaultState) + val state = _state.asStateFlow() - /** - * Manage consistency of internal state machine state and limits invocation of listeners to a minimum of one at a time. - */ - private val operationQueue = newFixedThreadPoolContext(1, "Single threaded dispatcher") - private val exceptionHandler = CoroutineExceptionHandler { _, exception -> - println("CoroutineExceptionHandler got $exception") - } + // Private accessor for the current state. Although this is thread-safe to access/mutate, we still want to limit + // read/write to the single-threaded stateMachineContext for consistency + private var currentState: StateType + get() = _state.value + set(value) { + _state.value = value + } - /** - * TODO: add coroutine exception handler if required. - */ - private val stateMachineScope = Job() + operationQueue // + exceptionHandler + // Manage consistency of internal state machine state and limits invocation of listeners to a minimum of one at a time. + @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) + private val stateMachineContext = SupervisorJob() + newSingleThreadContext("StateMachineContext") + private val stateMachineScope = CoroutineScope(stateMachineContext) // weak wrapper ?? - private val subscribers: MutableMap Unit> + private val subscribers: MutableMap Unit> = mutableMapOf() // atomic value ?? - private val pendingCancellations: MutableSet - - init { - val resolvedQueue = concurrentQueue ?: Dispatchers.Default - dispatcherQueue = resolvedQueue - this.executor = executor ?: ConcurrentEffectExecutor(resolvedQueue) - - subscribers = mutableMapOf() - pendingCancellations = mutableSetOf() - } + private val pendingCancellations: MutableSet = mutableSetOf() /** * Start listening to state changes updates. Asynchronously invoke listener on a background queue with the current state. @@ -94,8 +89,9 @@ internal open class StateMachine Unit, onSubscribe: OnSubscribedCallback?) { - GlobalScope.launch(stateMachineScope) { + stateMachineScope.launch { addSubscription(token, listener, onSubscribe) } } @@ -105,9 +101,10 @@ internal open class StateMachine Unit) { - GlobalScope.launch(stateMachineScope) { + stateMachineScope.launch { completion(currentState) } } + /** + * Get the current state, dispatching to the state machine context for the read. + */ + suspend fun getCurrentState() = withContext(stateMachineContext) { currentState } + /** * Register a listener. * @param token token, which will be retained in the subscribers map @@ -137,7 +140,7 @@ internal open class StateMachine? + val parameters: Map?, + val availableChallenges: List? = null +) { + override fun toString(): String = "AuthChallenge(" + + "challengeName='$challengeName', " + + "username=$username, " + + "session=${session.mask()}, " + + "parameters=${parameters?.maskSensitiveChallengeParameters()}, " + + "availableChallenges=$availableChallenges" + + ")" +} + +internal val AuthChallenge.challengeNameType + get() = ChallengeNameType.fromValue(challengeName) + +internal fun AuthChallenge.getParameter(parameter: ChallengeParameter) = parameters?.get(parameter.key) + +internal fun Map.maskSensitiveChallengeParameters() = mask( + ChallengeParameter.CodeDeliveryDestination.key, + ChallengeParameter.CredentialRequestOptions.key ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/ChallengeParameter.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/ChallengeParameter.kt new file mode 100644 index 0000000000..a9b1a8ce64 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/ChallengeParameter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.statemachine.codegen.data + +/** + * Enumeration of possible challenge parameter keys + */ +internal enum class ChallengeParameter(val key: String) { + CodeDeliveryDestination("CODE_DELIVERY_DESTINATION"), + CodeDeliveryMedium("CODE_DELIVERY_DELIVERY_MEDIUM"), + CredentialRequestOptions("CREDENTIAL_REQUEST_OPTIONS"), + MfasCanChoose("MFAS_CAN_CHOOSE"), + MfasCanSetup("MFAS_CAN_SETUP") +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInData.kt index 91b0c583d8..d27146b60e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInData.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInData.kt @@ -15,12 +15,18 @@ package com.amplifyframework.statemachine.codegen.data +import android.app.Activity +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.options.AuthFlowType +import java.lang.ref.WeakReference + internal sealed class SignInData { data class SRPSignInData( val username: String?, val password: String?, - val metadata: Map + val metadata: Map, + val authFlowType: AuthFlowType ) : SignInData() data class CustomAuthSignInData( @@ -31,7 +37,8 @@ internal sealed class SignInData { data class MigrationAuthSignInData( val username: String?, val password: String?, - val metadata: Map + val metadata: Map, + val authFlowType: AuthFlowType ) : SignInData() data class CustomSRPAuthSignInData( @@ -43,4 +50,18 @@ internal sealed class SignInData { data class HostedUISignInData( val hostedUIOptions: HostedUIOptions ) : SignInData() + + data class UserAuthSignInData( + val username: String?, + val preferredChallenge: AuthFactorType?, + val callingActivity: WeakReference, + val metadata: Map + ) : SignInData() + + data class AutoSignInData( + val username: String, + val session: String?, + val metadata: Map, + val userId: String? + ) : SignInData() } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInMethod.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInMethod.kt index d3e81bbcfa..2db142a29b 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInMethod.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInMethod.kt @@ -30,6 +30,7 @@ internal sealed class SignInMethod { USER_SRP_AUTH, CUSTOM_AUTH, USER_PASSWORD_AUTH, + USER_AUTH, UNKNOWN } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt index 284cd6ce80..7948fe56dd 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt @@ -14,6 +14,8 @@ */ package com.amplifyframework.statemachine.codegen.data +import com.amplifyframework.statemachine.util.mask + internal data class SignInTOTPSetupData( val secretCode: String, val session: String?, @@ -21,17 +23,9 @@ internal data class SignInTOTPSetupData( ) { override fun toString(): String { return "SignInTOTPSetupData(" + - "secretCode = ${mask(secretCode)}, " + - "session = ${mask(session)}, " + + "secretCode = ${secretCode.mask()}, " + + "session = ${session.mask()}, " + "username = $username}" + ")" } - - private fun mask(value: String?): String { - return if (value == null || value.length <= 4) { - "***" - } else { - "${value.substring(0 until 4)}***" - } - } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignUpData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignUpData.kt new file mode 100644 index 0000000000..6fd4cba6f4 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignUpData.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.statemachine.codegen.data + +import com.amplifyframework.statemachine.util.mask +import kotlinx.serialization.Serializable + +@Serializable +internal data class SignUpData( + val username: String, + val validationData: Map? = null, + val clientMetadata: Map? = null, + val session: String? = null, + val userId: String? = null +) { + override fun toString(): String = "SignUpData(" + + "username='$username', " + + "validationData=$validationData, " + + "clientMetadata=$clientMetadata, " + + "session=${session.mask()}, " + + "userId=$userId" + + ")" +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/WebAuthnSignInContext.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/WebAuthnSignInContext.kt new file mode 100644 index 0000000000..38acbe4839 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/WebAuthnSignInContext.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.statemachine.codegen.data + +import android.app.Activity +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.statemachine.util.mask +import java.lang.ref.WeakReference + +/** + * Class that accumulates the information needed to sign in via WebAuthn. The data may be built up over time. + */ +internal data class WebAuthnSignInContext( + val username: String, + val callingActivity: WeakReference, + val session: String?, + val requestJson: String? = null, + val responseJson: String? = null +) { + override fun toString() = "WebAuthnSignInContext(" + + "username='$username', " + + "callingActivity='$callingActivity', " + + "session='${session.mask()}', " + + "requestJson='${requestJson.mask()}', " + + "responseJson='${responseJson.mask()}'" + + ")" +} + +internal fun WebAuthnSignInContext.requireRequestJson(): String = + requestJson ?: throw InvalidStateException("Missing request json") +internal fun WebAuthnSignInContext.requireResponseJson(): String = + responseJson ?: throw InvalidStateException("Missing response json") diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SRPEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SRPEvent.kt index 838c99e2a5..9769519fb9 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SRPEvent.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SRPEvent.kt @@ -15,7 +15,9 @@ package com.amplifyframework.statemachine.codegen.events +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AuthChallenge import java.util.Date internal class SRPEvent(val eventType: EventType, override val time: Date? = null) : @@ -24,7 +26,9 @@ internal class SRPEvent(val eventType: EventType, override val time: Date? = nul data class InitiateSRP( val username: String, val password: String, - val metadata: Map + val metadata: Map, + val authFlowType: AuthFlowType, + val respondToAuthChallenge: AuthChallenge? = null ) : EventType() data class InitiateSRPWithCustom( val username: String, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt index 14044d1da6..3822c0ad48 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt @@ -15,12 +15,17 @@ package com.amplifyframework.statemachine.codegen.events +import android.app.Activity +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.statemachine.StateMachineEvent import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.SignInData import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData import com.amplifyframework.statemachine.codegen.data.SignedInData +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import java.lang.ref.WeakReference import java.util.Date internal class SignInEvent(val eventType: EventType, override val time: Date? = null) : StateMachineEvent { @@ -28,13 +33,12 @@ internal class SignInEvent(val eventType: EventType, override val time: Date? = data class InitiateSignInWithSRP( val username: String, val password: String, - val metadata: Map + val metadata: Map, + val authFlowType: AuthFlowType, + val respondToAuthChallenge: AuthChallenge? = null ) : EventType() - data class InitiateSignInWithCustom( - val username: String, - val metadata: Map - ) : EventType() + data class InitiateSignInWithCustom(val username: String, val metadata: Map) : EventType() data class InitiateCustomSignInWithSRP( val username: String, @@ -45,20 +49,17 @@ internal class SignInEvent(val eventType: EventType, override val time: Date? = data class InitiateMigrateAuth( val username: String, val password: String, - val metadata: Map + val metadata: Map, + val authFlowType: AuthFlowType, + val respondToAuthChallenge: AuthChallenge? = null ) : EventType() data class InitiateHostedUISignIn(val hostedUISignInData: SignInData.HostedUISignInData) : EventType() data class SignedIn(val id: String = "") : EventType() - data class InitiateSignInWithDeviceSRP( - val username: String, - val metadata: Map - ) : EventType() + data class InitiateSignInWithDeviceSRP(val username: String, val metadata: Map) : EventType() - data class ConfirmDevice( - val deviceMetadata: DeviceMetadata.Metadata, - val signedInData: SignedInData - ) : EventType() + data class ConfirmDevice(val deviceMetadata: DeviceMetadata.Metadata, val signedInData: SignedInData) : + EventType() data class FinalizeSignIn(val id: String = "") : EventType() data class ReceivedChallenge(val challenge: AuthChallenge) : EventType() data class ThrowError(val exception: Exception) : EventType() @@ -66,6 +67,15 @@ internal class SignInEvent(val eventType: EventType, override val time: Date? = val signInTOTPSetupData: SignInTOTPSetupData, val challengeParams: Map? ) : EventType() + + data class InitiateUserAuth( + val username: String, + val preferredChallenge: AuthFactorType?, + val callingActivity: WeakReference, + val metadata: Map + ) : EventType() + data class InitiateWebAuthnSignIn(val signInContext: WebAuthnSignInContext) : EventType() + data class InitiateAutoSignIn(val signInData: SignInData.AutoSignInData) : EventType() } override val type: String = eventType.javaClass.simpleName diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignUpEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignUpEvent.kt new file mode 100644 index 0000000000..bf463bb160 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignUpEvent.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.statemachine.codegen.events + +import com.amplifyframework.auth.AuthUserAttribute +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.SignUpData +import java.util.Date + +internal class SignUpEvent( + val eventType: EventType, + override val time: Date? = null, +) : StateMachineEvent { + sealed class EventType { + data class InitiateSignUp( + val signUpData: SignUpData, + val password: String? = null, + val userAttributes: List? = null + ) : EventType() + + data class InitiateSignUpComplete(val signUpData: SignUpData, val signUpResult: AuthSignUpResult) : EventType() + + data class ConfirmSignUp(val signUpData: SignUpData, val confirmationCode: String) : EventType() + + data class SignedUp(val signUpData: SignUpData, val signUpResult: AuthSignUpResult) : EventType() + + data class ThrowError(val exception: Exception) : EventType() + } + + override val type: String = eventType.javaClass.simpleName +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/WebAuthnEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/WebAuthnEvent.kt new file mode 100644 index 0000000000..532840f5c5 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/WebAuthnEvent.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.statemachine.codegen.events + +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.events.SignInEvent.EventType +import java.util.Date + +internal class WebAuthnEvent(val eventType: EventType, override val time: Date? = null) : StateMachineEvent { + + sealed class EventType { + data class FetchCredentialOptions(val signInContext: WebAuthnSignInContext) : EventType() + data class AssertCredentialOptions(val signInContext: WebAuthnSignInContext) : EventType() + data class VerifyCredentialsAndSignIn(val signInContext: WebAuthnSignInContext) : EventType() + data class ThrowError(val exception: Exception, val signInContext: WebAuthnSignInContext) : EventType() + } + + override val type: String = eventType.javaClass.simpleName +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthState.kt index e4a5845069..c3439e7857 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthState.kt @@ -26,7 +26,9 @@ import com.amplifyframework.statemachine.codegen.events.AuthEvent internal sealed class AuthState : State { data class NotConfigured(val id: String = "") : AuthState() data class ConfiguringAuth(val id: String = "") : AuthState() - data class ConfiguringAuthentication(override var authNState: AuthenticationState?) : AuthState() + data class ConfiguringAuthentication( + override var authNState: AuthenticationState? + ) : AuthState() data class ConfiguringAuthorization( override var authNState: AuthenticationState?, @@ -35,18 +37,21 @@ internal sealed class AuthState : State { data class Configured( override var authNState: AuthenticationState?, - override var authZState: AuthorizationState? + override var authZState: AuthorizationState?, + override var authSignUpState: SignUpState? ) : AuthState() data class Error(val exception: Exception) : AuthState() open var authNState: AuthenticationState? = AuthenticationState.NotConfigured() open var authZState: AuthorizationState? = AuthorizationState.NotConfigured() + open var authSignUpState: SignUpState? = SignUpState.NotStarted() class Resolver( private val authNResolver: StateMachineResolver, private val authZResolver: StateMachineResolver, - private val authActions: AuthActions + private val authActions: AuthActions, + private val authSignUpResolver: StateMachineResolver ) : StateMachineResolver { override val defaultState = NotConfigured() @@ -66,6 +71,11 @@ internal sealed class AuthState : State { actions += it.actions } + oldState.authSignUpState?.let { authSignUpResolver.resolve(it, event) }?.let { + builder.authSignUpState = it.newState + actions += it.actions + } + return StateResolution(builder.build(), actions) } @@ -106,7 +116,7 @@ internal sealed class AuthState : State { } is ConfiguringAuthorization -> when (authEvent) { is AuthEvent.EventType.ConfiguredAuthorization -> StateResolution( - Configured(oldState.authNState, oldState.authZState) + Configured(oldState.authNState, oldState.authZState, oldState.authSignUpState) ) else -> defaultResolution } @@ -119,11 +129,12 @@ internal sealed class AuthState : State { com.amplifyframework.statemachine.Builder { var authNState: AuthenticationState? = null var authZState: AuthorizationState? = null + var authSignUpState: SignUpState? = null override fun build() = when (authState) { is ConfiguringAuthentication -> ConfiguringAuthentication(authNState) is ConfiguringAuthorization -> ConfiguringAuthorization(authNState, authZState) - is Configured -> Configured(authNState, authZState) + is Configured -> Configured(authNState, authZState, authSignUpState) else -> authState } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthenticationState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthenticationState.kt index bdfb3775e7..a14dda7290 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthenticationState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthenticationState.kt @@ -80,6 +80,10 @@ internal sealed class AuthenticationState : State { is AuthenticationEvent.EventType.InitializedSignedOut -> StateResolution( SignedOut(authenticationEvent.signedOutData) ) + is AuthenticationEvent.EventType.SignInRequested -> { + val action = authenticationActions.initiateSignInAction(authenticationEvent) + StateResolution(SigningIn(), listOf(action)) + } else -> defaultResolution } is SigningIn -> when (authenticationEvent) { @@ -92,6 +96,9 @@ internal sealed class AuthenticationState : State { } StateResolution(SignedOut(SignedOutData())) } + is AuthenticationEvent.EventType.ThrowError -> { + StateResolution(Error(authenticationEvent.exception)) + } else -> { val resolution = signInResolver.resolve(oldState.signInState, event) StateResolution(SigningIn(resolution.newState), resolution.actions) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthorizationState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthorizationState.kt index c3e3bd7af7..5f699e4547 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthorizationState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/AuthorizationState.kt @@ -84,6 +84,15 @@ internal sealed class AuthorizationState : State { val authorizationEvent = event.isAuthorizationEvent() val deleteUserEvent = event.isDeleteUserEvent() val defaultResolution = StateResolution(oldState) + + if (authenticationEvent is AuthenticationEvent.EventType.SignInCompleted) { + val action = authorizationActions.initializeFetchAuthSession(authenticationEvent.signedInData) + return StateResolution( + FetchingAuthSession(authenticationEvent.signedInData, FetchAuthSessionState.NotStarted()), + listOf(action) + ) + } + return when (oldState) { is NotConfigured -> when (authorizationEvent) { is AuthorizationEvent.EventType.Configure -> { 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 3dad9cda40..600301accb 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 @@ -58,6 +58,25 @@ internal sealed class SignInChallengeState : State { else -> defaultResolution } is WaitingForAnswer -> when (challengeEvent) { + is SignInChallengeEvent.EventType.WaitForAnswer -> { + /** + * This sends out a second WaitingForAnswer because it requires an additional user-response + * before calling RespondToAuth. e.g. the first WaitingForAnswer asks the user to select + * a first-factor challenge and the user selects password. The user needs to supply the password + * so that the answer *and* the password can be sent in one RespondToAuth call. + **/ + StateResolution( + WaitingForAnswer( + challenge = AuthChallenge( + challengeName = challengeEvent.challenge.challengeName, + username = oldState.challenge.username, + session = oldState.challenge.session, + parameters = oldState.challenge.parameters + ), + hasNewResponse = true + ) + ) + } is SignInChallengeEvent.EventType.VerifyChallengeAnswer -> { val action = challengeActions.verifyChallengeAuthAction( challengeEvent.answer, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt index b5317633b1..35a1a9332c 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt @@ -20,6 +20,8 @@ import com.amplifyframework.statemachine.StateMachineEvent import com.amplifyframework.statemachine.StateMachineResolver import com.amplifyframework.statemachine.StateResolution import com.amplifyframework.statemachine.codegen.actions.SignInActions +import com.amplifyframework.statemachine.codegen.actions.UserAuthSignInActions +import com.amplifyframework.statemachine.codegen.data.SignInData import com.amplifyframework.statemachine.codegen.events.SignInEvent internal sealed class SignInState : State { @@ -29,6 +31,8 @@ internal sealed class SignInState : State { data class SigningInWithCustom(override var customSignInState: CustomSignInState?) : SignInState() data class SigningInWithSRPCustom(override var srpSignInState: SRPSignInState?) : SignInState() data class SigningInViaMigrateAuth(override var migrateSignInState: MigrateSignInState?) : SignInState() + data class SigningInWithUserAuth(val id: String = "") : SignInState() + data class SigningInWithWebAuthn(override var webAuthnSignInState: WebAuthnSignInState?) : SignInState() data class ResolvingDeviceSRP(override var deviceSRPSignInState: DeviceSRPSignInState?) : SignInState() data class ResolvingChallenge(override var challengeState: SignInChallengeState?) : SignInState() data class ResolvingTOTPSetup(override var setupTOTPState: SetupTOTPState?) : SignInState() @@ -36,6 +40,7 @@ internal sealed class SignInState : State { data class Done(val id: String = "") : SignInState() data class Error(val exception: Exception) : SignInState() data class SignedIn(val id: String = "") : SignInState() + data class AutoSigningIn(val signInEventData: SignInData.AutoSignInData) : SignInState() open var srpSignInState: SRPSignInState? = SRPSignInState.NotStarted() open var challengeState: SignInChallengeState? = SignInChallengeState.NotStarted() @@ -44,6 +49,7 @@ internal sealed class SignInState : State { open var hostedUISignInState: HostedUISignInState? = HostedUISignInState.NotStarted() open var deviceSRPSignInState: DeviceSRPSignInState? = DeviceSRPSignInState.NotStarted() open var setupTOTPState: SetupTOTPState? = SetupTOTPState.NotStarted() + open var webAuthnSignInState: WebAuthnSignInState? = WebAuthnSignInState.NotStarted() class Resolver( private val srpSignInResolver: StateMachineResolver, @@ -53,14 +59,13 @@ internal sealed class SignInState : State { private val hostedUISignInResolver: StateMachineResolver, private val deviceSRPSignInResolver: StateMachineResolver, private val setupTOTPResolver: StateMachineResolver, + private val webAuthnSignInResolver: StateMachineResolver, + private val userAuthSignInActions: UserAuthSignInActions, private val signInActions: SignInActions - ) : - StateMachineResolver { + ) : StateMachineResolver { override val defaultState = NotStarted() - private fun asSignInEvent(event: StateMachineEvent): SignInEvent.EventType? { - return (event as? SignInEvent)?.eventType - } + private fun asSignInEvent(event: StateMachineEvent): SignInEvent.EventType? = (event as? SignInEvent)?.eventType override fun resolve(oldState: SignInState, event: StateMachineEvent): StateResolution { val resolution = resolveSignInEvent(oldState, event) @@ -101,13 +106,16 @@ internal sealed class SignInState : State { builder.setupTOTPState = it.newState actions += it.actions } + + oldState.webAuthnSignInState?.let { webAuthnSignInResolver.resolve(it, event) }?.let { + builder.webAuthnSignInState = it.newState + actions += it.actions + } + return StateResolution(builder.build(), actions) } - private fun resolveSignInEvent( - oldState: SignInState, - event: StateMachineEvent - ): StateResolution { + private fun resolveSignInEvent(oldState: SignInState, event: StateMachineEvent): StateResolution { val signInEvent = asSignInEvent(event) val defaultResolution = StateResolution(oldState) return when (oldState) { @@ -137,11 +145,22 @@ internal sealed class SignInState : State { listOf(signInActions.startCustomAuthWithSRPAction(signInEvent)) ) + is SignInEvent.EventType.InitiateUserAuth -> { + StateResolution( + SigningInWithUserAuth(), + listOf(userAuthSignInActions.initiateUserAuthSignIn(signInEvent)) + ) + } + + is SignInEvent.EventType.InitiateAutoSignIn -> StateResolution( + AutoSigningIn(signInEvent.signInData), + listOf(signInActions.autoSignInAction(signInEvent)) + ) else -> defaultResolution } is SigningInWithSRP, is SigningInWithCustom, is SigningInViaMigrateAuth, - is SigningInWithSRPCustom + is SigningInWithSRPCustom, is SigningInWithUserAuth, is SigningInWithWebAuthn -> when (signInEvent) { is SignInEvent.EventType.ReceivedChallenge -> { val action = signInActions.initResolveChallenge(signInEvent) @@ -163,6 +182,11 @@ internal sealed class SignInState : State { listOf(signInActions.initiateTOTPSetupAction(signInEvent)) ) + is SignInEvent.EventType.InitiateWebAuthnSignIn -> StateResolution( + newState = SigningInWithWebAuthn(WebAuthnSignInState.NotStarted()), + actions = listOf(signInActions.initiateWebAuthnSignInAction(signInEvent)) + ) + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } @@ -183,6 +207,16 @@ internal sealed class SignInState : State { listOf(signInActions.initiateTOTPSetupAction(signInEvent)) ) + is SignInEvent.EventType.InitiateWebAuthnSignIn -> StateResolution( + newState = SigningInWithWebAuthn(WebAuthnSignInState.NotStarted()), + actions = listOf(signInActions.initiateWebAuthnSignInAction(signInEvent)) + ) + + is SignInEvent.EventType.InitiateSignInWithSRP -> StateResolution( + SigningInWithSRP(oldState.srpSignInState), + listOf(signInActions.startSRPAuthAction(signInEvent)) + ) + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } @@ -240,7 +274,14 @@ internal sealed class SignInState : State { is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } + is AutoSigningIn -> when (signInEvent) { + is SignInEvent.EventType.FinalizeSignIn -> { + StateResolution(SignedIn()) + } + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) + else -> defaultResolution + } else -> defaultResolution } } @@ -255,6 +296,7 @@ internal sealed class SignInState : State { var hostedUISignInState: HostedUISignInState? = null var deviceSRPSignInState: DeviceSRPSignInState? = null var setupTOTPState: SetupTOTPState? = null + var webAuthnSignInState: WebAuthnSignInState? = null override fun build(): SignInState = when (signInState) { is SigningInWithSRP -> SigningInWithSRP(srpSignInState) @@ -265,6 +307,8 @@ internal sealed class SignInState : State { is SigningInWithSRPCustom -> SigningInWithSRPCustom(srpSignInState) is ResolvingDeviceSRP -> ResolvingDeviceSRP(deviceSRPSignInState) is ResolvingTOTPSetup -> ResolvingTOTPSetup(setupTOTPState) + is SigningInWithUserAuth -> SigningInWithUserAuth() + is SigningInWithWebAuthn -> SigningInWithWebAuthn(webAuthnSignInState) else -> signInState } } 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 new file mode 100644 index 0000000000..6e622f8f44 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignUpState.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2024 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.statemachine.codegen.states + +import com.amplifyframework.auth.cognito.isSignUpEvent +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.statemachine.State +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.StateMachineResolver +import com.amplifyframework.statemachine.StateResolution +import com.amplifyframework.statemachine.codegen.actions.SignUpActions +import com.amplifyframework.statemachine.codegen.data.SignUpData +import com.amplifyframework.statemachine.codegen.events.SignUpEvent + +internal sealed class SignUpState : State { + data class NotStarted(val id: String = "") : SignUpState() + data class InitiatingSignUp(val signUpData: SignUpData) : SignUpState() + data class AwaitingUserConfirmation(val signUpData: SignUpData, val signUpResult: AuthSignUpResult) : SignUpState() + data class ConfirmingSignUp(val signUpData: SignUpData) : SignUpState() + data class SignedUp(val signUpData: SignUpData, val signUpResult: AuthSignUpResult) : SignUpState() + data class Error(val exception: Exception, var hasNewResponse: Boolean = true) : SignUpState() + + class Resolver(private val signUpActions: SignUpActions) : + StateMachineResolver { + override val defaultState = NotStarted("") + + override fun resolve(oldState: SignUpState, event: StateMachineEvent): StateResolution { + val defaultResolution = StateResolution(oldState) + val signUpEvent = event.isSignUpEvent() + + return when (oldState) { + is NotStarted, is SignedUp -> when (signUpEvent) { + is SignUpEvent.EventType.InitiateSignUp -> { + StateResolution( + InitiatingSignUp(signUpEvent.signUpData), + listOf(signUpActions.initiateSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ConfirmSignUp -> { + StateResolution( + ConfirmingSignUp(signUpEvent.signUpData), + listOf(signUpActions.confirmSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ThrowError -> { + StateResolution(Error(signUpEvent.exception)) + } + else -> defaultResolution + } + is InitiatingSignUp -> when (signUpEvent) { + is SignUpEvent.EventType.InitiateSignUp -> { + StateResolution( + InitiatingSignUp(signUpEvent.signUpData), + listOf(signUpActions.initiateSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.InitiateSignUpComplete -> { + StateResolution(AwaitingUserConfirmation(signUpEvent.signUpData, signUpEvent.signUpResult)) + } + is SignUpEvent.EventType.ConfirmSignUp -> { + StateResolution( + ConfirmingSignUp(signUpEvent.signUpData), + listOf(signUpActions.confirmSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.SignedUp -> { + StateResolution( + SignedUp(signUpEvent.signUpData, signUpEvent.signUpResult) + ) + } + is SignUpEvent.EventType.ThrowError -> { + StateResolution(Error(signUpEvent.exception)) + } + else -> defaultResolution + } + is AwaitingUserConfirmation -> when (signUpEvent) { + is SignUpEvent.EventType.InitiateSignUp -> { + StateResolution( + InitiatingSignUp(signUpEvent.signUpData), + listOf(signUpActions.initiateSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ConfirmSignUp -> { + StateResolution( + ConfirmingSignUp(signUpEvent.signUpData), + listOf(signUpActions.confirmSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ThrowError -> { + StateResolution(Error(signUpEvent.exception)) + } + else -> defaultResolution + } + is ConfirmingSignUp -> when (signUpEvent) { + is SignUpEvent.EventType.InitiateSignUp -> { + StateResolution( + InitiatingSignUp(signUpEvent.signUpData), + listOf(signUpActions.initiateSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ConfirmSignUp -> { + StateResolution( + ConfirmingSignUp(signUpEvent.signUpData), + listOf(signUpActions.confirmSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.SignedUp -> { + StateResolution(SignedUp(signUpEvent.signUpData, signUpEvent.signUpResult)) + } + is SignUpEvent.EventType.ThrowError -> { + StateResolution(Error(signUpEvent.exception)) + } + else -> defaultResolution + } + is Error -> when (signUpEvent) { + is SignUpEvent.EventType.InitiateSignUp -> { + StateResolution( + InitiatingSignUp(signUpEvent.signUpData), + listOf(signUpActions.initiateSignUpAction(signUpEvent)) + ) + } + is SignUpEvent.EventType.ConfirmSignUp -> { + StateResolution( + ConfirmingSignUp(signUpEvent.signUpData), + listOf(signUpActions.confirmSignUpAction(signUpEvent)) + ) + } + else -> defaultResolution + } + } + } + } +} 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 new file mode 100644 index 0000000000..57bb149fdc --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/WebAuthnSignInState.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024 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.statemachine.codegen.states + +import com.amplifyframework.statemachine.State +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.StateMachineResolver +import com.amplifyframework.statemachine.StateResolution +import com.amplifyframework.statemachine.codegen.actions.SignInActions +import com.amplifyframework.statemachine.codegen.actions.WebAuthnSignInActions +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import com.amplifyframework.statemachine.codegen.events.WebAuthnEvent + +internal sealed class WebAuthnSignInState : State { + data class NotStarted(val id: String = "") : WebAuthnSignInState() + data class FetchingCredentialOptions(val id: String = "") : WebAuthnSignInState() + 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, var hasNewResponse: Boolean) : + WebAuthnSignInState() + + class Resolver(private val actions: WebAuthnSignInActions, private val signInActions: SignInActions) : + StateMachineResolver { + override val defaultState = NotStarted() + + override fun resolve( + oldState: WebAuthnSignInState, + event: StateMachineEvent + ): StateResolution { + val defaultResolution = StateResolution(oldState) + val webAuthnEvent = event.asWebAuthnSignInEvent() + + // Thrown errors always result in the error state + if (webAuthnEvent is WebAuthnEvent.EventType.ThrowError) { + return StateResolution( + Error(webAuthnEvent.exception, webAuthnEvent.signInContext, hasNewResponse = true) + ) + } + + return when (oldState) { + is NotStarted -> when (webAuthnEvent) { + is WebAuthnEvent.EventType.AssertCredentialOptions -> StateResolution( + newState = AssertingCredentials(), + actions = listOf(actions.assertCredentials(webAuthnEvent)) + ) + is WebAuthnEvent.EventType.FetchCredentialOptions -> StateResolution( + newState = FetchingCredentialOptions(), + actions = listOf(actions.fetchCredentialOptions(webAuthnEvent)) + ) + else -> defaultResolution + } + is FetchingCredentialOptions -> when (webAuthnEvent) { + is WebAuthnEvent.EventType.AssertCredentialOptions -> StateResolution( + newState = AssertingCredentials(), + actions = listOf(actions.assertCredentials(webAuthnEvent)) + ) + else -> defaultResolution + } + is AssertingCredentials -> when (webAuthnEvent) { + is WebAuthnEvent.EventType.VerifyCredentialsAndSignIn -> StateResolution( + newState = VerifyingCredentialsAndSigningIn(), + actions = listOf(actions.verifyCredentialAndSignIn(webAuthnEvent)) + ) + else -> defaultResolution + } + is VerifyingCredentialsAndSigningIn -> defaultResolution + is SignedIn -> defaultResolution + is Error -> when { + event is SignInEvent && event.eventType is SignInEvent.EventType.InitiateWebAuthnSignIn -> + StateResolution( + newState = NotStarted(), + actions = listOf(signInActions.initiateWebAuthnSignInAction(event.eventType)) + ) + else -> defaultResolution + } + } + } + + private fun StateMachineEvent.asWebAuthnSignInEvent() = (this as? WebAuthnEvent)?.eventType + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/util/MaskUtil.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/util/MaskUtil.kt new file mode 100644 index 0000000000..d2fa40b254 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/util/MaskUtil.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.statemachine.util + +internal fun String?.mask() = if (this == null || this.length <= 4) { + "***" +} else { + "${this.substring(0 until 4)}***" +} + +/** + * Masks the values of the given keys in the map, while leaving other values unmasked + */ +internal fun Map.mask(vararg keys: String): Map = mapValues { (key, value) -> + if (keys.contains(key)) value.mask() else value +} 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 66f0ef0ceb..c6049ef760 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 @@ -26,13 +26,17 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.options.AuthResetPasswordOptions @@ -49,6 +53,7 @@ import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.AuthUpdateAttributeResult import com.amplifyframework.core.Action import com.amplifyframework.core.Consumer +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -69,6 +74,7 @@ class AWSCognitoAuthPluginTest { fun setup() { authPlugin = AWSCognitoAuthPlugin() authPlugin.realPlugin = realPlugin + authPlugin.useCaseFactory = mockk(relaxed = true) } @Test @@ -743,6 +749,69 @@ class AWSCognitoAuthPluginTest { } } + @Test + fun associateWebAuthnCredential() { + val useCase = authPlugin.useCaseFactory.associateWebAuthnCredential() + + val activity: Activity = mockk() + authPlugin.associateWebAuthnCredential(activity, {}, {}) + coVerify(timeout = CHANNEL_TIMEOUT) { + useCase.execute(activity, AuthAssociateWebAuthnCredentialsOptions.defaults()) + } + } + + @Test + fun associateWebAuthnCredentialWithOptions() { + val useCase = authPlugin.useCaseFactory.associateWebAuthnCredential() + + val activity: Activity = mockk() + val options: AuthAssociateWebAuthnCredentialsOptions = mockk() + authPlugin.associateWebAuthnCredential(activity, options, {}, {}) + coVerify(timeout = CHANNEL_TIMEOUT) { + useCase.execute(activity, options) + } + } + + @Test + fun listWebAuthnCredentials() { + val useCase = authPlugin.useCaseFactory.listWebAuthnCredentials() + authPlugin.listWebAuthnCredentials({}, {}) + coVerify { + useCase.execute(AuthListWebAuthnCredentialsOptions.defaults()) + } + } + + @Test + fun listWebAuthnCredentialsWithOptions() { + val useCase = authPlugin.useCaseFactory.listWebAuthnCredentials() + val options = AWSCognitoAuthListWebAuthnCredentialsOptions.builder().build() + authPlugin.listWebAuthnCredentials(options, {}, {}) + coVerify { + useCase.execute(options) + } + } + + @Test + fun deleteWebAuthnCredential() { + val useCase = authPlugin.useCaseFactory.deleteWebAuthnCredential() + val credentialId = "someId" + authPlugin.deleteWebAuthnCredential(credentialId, {}, {}) + coVerify { + useCase.execute(credentialId, AuthDeleteWebAuthnCredentialOptions.defaults()) + } + } + + @Test + fun deleteWebAuthnCredentialWithOptions() { + val useCase = authPlugin.useCaseFactory.deleteWebAuthnCredential() + val options: AuthDeleteWebAuthnCredentialOptions = mockk() + val credentialId = "someId" + authPlugin.deleteWebAuthnCredential(credentialId, options, {}, {}) + coVerify { + useCase.execute(credentialId, options) + } + } + @Test fun verifyPluginKey() { assertEquals("awsCognitoAuthPlugin", authPlugin.pluginKey) 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 bc4a266e82..bd7dec79fc 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 @@ -36,6 +36,7 @@ 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 com.amplifyframework.statemachine.codegen.states.SignUpState import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every @@ -121,7 +122,8 @@ class AuthValidationTest { environment, initialState = AuthState.Configured( authNState = AuthenticationState.SignedOut(signedOutData = SignedOutData()), - authZState = AuthorizationState.Configured() + authZState = AuthorizationState.Configured(), + authSignUpState = SignUpState.NotStarted() ) ) 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 new file mode 100644 index 0000000000..a077c5418e --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/MockData.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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 + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.WebAuthnCredentialDescription +import aws.smithy.kotlin.runtime.time.Instant +import com.amplifyframework.auth.AuthCodeDeliveryDetails +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.result.step.AuthNextSignInStep +import com.amplifyframework.auth.result.step.AuthSignInStep + +fun mockWebAuthnCredentialDescription( + credentialId: String = "id", + friendlyName: String = "name", + relyingParty: String = "relyingParty", + createdAt: Instant = Instant.now() +) = WebAuthnCredentialDescription { + this.credentialId = credentialId + this.createdAt = createdAt + this.relyingPartyId = relyingParty + friendlyCredentialName = friendlyName + authenticatorTransports = emptyList() +} + +fun mockAuthNextSignInStep( + authSignInStep: AuthSignInStep = AuthSignInStep.DONE, + additionalInfo: Map = emptyMap(), + authCodeDeliveryDetails: AuthCodeDeliveryDetails? = null, + totpSetupDetails: TOTPSetupDetails? = null, + allowedMFATypes: Set? = null, + availableFactors: Set? = null +) = AuthNextSignInStep( + authSignInStep, + additionalInfo, + authCodeDeliveryDetails, + totpSetupDetails, + allowedMFATypes, + availableFactors +) 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 ad1466f85b..342274923e 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 @@ -26,8 +26,6 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchExcepti import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProviderException import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordResponse -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpRequest -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeliveryMediumType import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceType import aws.sdk.kotlin.services.cognitoidentityprovider.model.EmailMfaSettingsType @@ -39,8 +37,6 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationC import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationCodeResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.SetUserMfaPreferenceRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.SetUserMfaPreferenceResponse -import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpRequest -import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.SmsMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaNotFoundException import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaSettingsType @@ -83,8 +79,6 @@ import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.AuthUpdateAttributeResult -import com.amplifyframework.auth.result.step.AuthNextSignUpStep -import com.amplifyframework.auth.result.step.AuthSignUpStep import com.amplifyframework.auth.result.step.AuthUpdateAttributeStep import com.amplifyframework.core.Action import com.amplifyframework.core.Consumer @@ -693,183 +687,6 @@ class RealAWSCognitoAuthPluginTest { assertEquals(expectedException, onError.captured.cause) } - @Test - fun `test signup API with given arguments and auth signed in`() { - `test signup API with given arguments`() - } - - @Test - fun `test signup API with given arguments and auth signed out`() { - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - `test signup API with given arguments`() - } - - private fun `test signup API with given arguments`() { - val latch = CountDownLatch(1) - - // GIVEN - val username = "user" - val password = "password" - val email = "user@domain.com" - val options = AuthSignUpOptions.builder().userAttribute(AuthUserAttributeKey.email(), email).build() - - val requestCaptor = slot() - coEvery { authService.cognitoIdentityProviderClient?.signUp(capture(requestCaptor)) } coAnswers { - latch.countDown() - mockk() - } - - val expectedRequest: SignUpRequest.Builder.() -> Unit = { - clientId = "app Client Id" - this.username = username - this.password = password - userAttributes = listOf( - AttributeType { - name = "email" - value = email - } - ) - secretHash = "dummy Hash" - userContextData = null - analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = expectedEndpointId } - } - - // WHEN - plugin.signUp(username, password, options, {}, {}) - assertTrue { latch.await(5, TimeUnit.SECONDS) } - - // THEN - assertEquals(SignUpRequest.invoke(expectedRequest), requestCaptor.captured) - } - - @Test - fun `test signup success`() { - // GIVEN - val onSuccess = ConsumerWithLatch() - val userId = "123456" - val username = "user" - val password = "password" - val email = "user@domain.com" - val options = AuthSignUpOptions.builder().userAttribute(AuthUserAttributeKey.email(), email).build() - - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - - val deliveryDetails = mapOf( - "DESTINATION" to email, - "MEDIUM" to "EMAIL", - "ATTRIBUTE" to "attributeName" - ) - - val expectedAuthSignUpResult = AuthSignUpResult( - false, - AuthNextSignUpStep( - AuthSignUpStep.CONFIRM_SIGN_UP_STEP, - mapOf(), - AuthCodeDeliveryDetails( - deliveryDetails.getValue("DESTINATION"), - AuthCodeDeliveryDetails.DeliveryMedium.fromString(deliveryDetails.getValue("MEDIUM")), - deliveryDetails.getValue("ATTRIBUTE") - ) - ), - userId - ) - - coEvery { authService.cognitoIdentityProviderClient?.signUp(any()) } coAnswers { - SignUpResponse.invoke { - this.userSub = userId - this.codeDeliveryDetails { - this.attributeName = "attributeName" - this.deliveryMedium = DeliveryMediumType.Email - this.destination = email - } - } - } - - // WHEN - plugin.signUp(username, password, options, onSuccess, mockk()) - - // THEN - onSuccess.shouldBeCalled() - assertEquals(expectedAuthSignUpResult, onSuccess.captured) - } - - @Test - fun `test confirm signup API with given arguments and auth signed in`() { - `test confirm signup API with given arguments`() - } - - @Test - fun `test confirm signup API with given arguments and auth signed out`() { - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - `test confirm signup API with given arguments`() - } - - private fun `test confirm signup API with given arguments`() { - val latch = CountDownLatch(1) - - // GIVEN - val username = "user" - val confirmationCode = "123456" - val options = AuthConfirmSignUpOptions.defaults() - - val requestCaptor = slot() - coEvery { authService.cognitoIdentityProviderClient?.confirmSignUp(capture(requestCaptor)) } coAnswers { - latch.countDown() - mockk() - } - - val expectedRequest: ConfirmSignUpRequest.Builder.() -> Unit = { - clientId = "app Client Id" - this.username = username - this.confirmationCode = confirmationCode - secretHash = "dummy Hash" - userContextData = null - analyticsMetadata = AnalyticsMetadataType.invoke { analyticsEndpointId = expectedEndpointId } - } - - // WHEN - plugin.confirmSignUp(username, confirmationCode, options, {}, {}) - assertTrue { latch.await(5, TimeUnit.SECONDS) } - - // THEN - assertEquals(ConfirmSignUpRequest.invoke(expectedRequest), requestCaptor.captured) - } - - @Test - fun `test confirm signup success`() { - val latch = CountDownLatch(1) - - // GIVEN - val onSuccess = mockk>() - val onError = mockk>() - val username = "user" - val confirmationCode = "123456" - val options = AuthConfirmSignUpOptions.defaults() - - val resultCaptor = slot() - every { onSuccess.accept(capture(resultCaptor)) } answers { latch.countDown() } - - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - - val expectedAuthSignUpResult = AuthSignUpResult( - true, - AuthNextSignUpStep(AuthSignUpStep.DONE, mapOf(), null), - null - ) - - coEvery { authService.cognitoIdentityProviderClient?.confirmSignUp(any()) } coAnswers { - ConfirmSignUpResponse.invoke { } - } - - // WHEN - plugin.confirmSignUp(username, confirmationCode, options, onSuccess, onError) - assertTrue { latch.await(5, TimeUnit.SECONDS) } - - // THEN - verify(exactly = 0) { onError.accept(any()) } - verify(exactly = 1) { onSuccess.accept(expectedAuthSignUpResult) } - } - @Test fun `test resend signup code API with given arguments and auth signed out`() { setupCurrentAuthState(AuthenticationState.SignedOut(mockk())) @@ -1619,7 +1436,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, null, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + null, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -1662,7 +1483,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, null, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + null, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -1705,7 +1530,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } @@ -1778,7 +1607,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -1828,7 +1661,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.DISABLED, MFAPreference.DISABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.DISABLED, + MFAPreference.DISABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -1878,7 +1715,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk() + MFAPreference.DISABLED, + MFAPreference.ENABLED, + MFAPreference.DISABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -1928,7 +1769,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.DISABLED, + MFAPreference.DISABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } @@ -1978,7 +1823,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.PREFERRED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.PREFERRED, + MFAPreference.ENABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2028,7 +1877,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.PREFERRED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2078,7 +1931,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + MFAPreference.PREFERRED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } @@ -2128,7 +1985,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.PREFERRED, MFAPreference.DISABLED, onSuccess, mockk() + MFAPreference.DISABLED, + MFAPreference.PREFERRED, + MFAPreference.DISABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2178,7 +2039,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.PREFERRED, MFAPreference.DISABLED, MFAPreference.DISABLED, onSuccess, mockk() + MFAPreference.PREFERRED, + MFAPreference.DISABLED, + MFAPreference.DISABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2228,7 +2093,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.DISABLED, MFAPreference.PREFERRED, onSuccess, mockk() + MFAPreference.DISABLED, + MFAPreference.DISABLED, + MFAPreference.PREFERRED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } @@ -2278,7 +2147,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.DISABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2328,7 +2201,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.DISABLED, + MFAPreference.ENABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2378,7 +2255,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.ENABLED, + MFAPreference.DISABLED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } @@ -2429,7 +2310,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, onError + MFAPreference.DISABLED, + MFAPreference.ENABLED, + MFAPreference.DISABLED, + onSuccess, + onError ) onSuccess.shouldBeCalled() @@ -2479,7 +2364,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.PREFERRED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.PREFERRED, + MFAPreference.ENABLED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2529,7 +2418,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.PREFERRED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) onSuccess.shouldBeCalled() @@ -2579,7 +2472,11 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke {} } plugin.updateMFAPreference( - MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + MFAPreference.ENABLED, + MFAPreference.PREFERRED, + MFAPreference.ENABLED, + onSuccess, + mockk() ) assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt index f24f65c132..50706c9ab8 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt @@ -17,6 +17,7 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import com.amplifyframework.auth.cognito.actions.DeleteUserCognitoActions +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.AuthActions import com.amplifyframework.statemachine.codegen.actions.AuthenticationActions @@ -31,6 +32,9 @@ import com.amplifyframework.statemachine.codegen.actions.SetupTOTPActions import com.amplifyframework.statemachine.codegen.actions.SignInActions import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions import com.amplifyframework.statemachine.codegen.actions.SignOutActions +import com.amplifyframework.statemachine.codegen.actions.SignUpActions +import com.amplifyframework.statemachine.codegen.actions.UserAuthSignInActions +import com.amplifyframework.statemachine.codegen.actions.WebAuthnSignInActions import com.amplifyframework.statemachine.codegen.data.AWSCredentials import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.AuthChallenge @@ -120,6 +124,15 @@ open class StateTransitionTestBase { @Mock internal lateinit var mockSetupTOTPActions: SetupTOTPActions + @Mock + internal lateinit var mockSignUpActions: SignUpActions + + @Mock + internal lateinit var mockUserAuthSignInActions: UserAuthSignInActions + + @Mock + internal lateinit var mockWebAuthnSignInActions: WebAuthnSignInActions + private val dummyCredential = AmplifyCredential.UserAndIdentityPool( SignedInData( "userId", @@ -269,7 +282,8 @@ open class StateTransitionTestBase { SRPEvent.EventType.InitiateSRP( "username", "password", - mapOf() + mapOf(), + AuthFlowType.USER_SRP_AUTH ) ) ) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt index 7bca155442..55b361dc1c 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt @@ -16,6 +16,7 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.StateChangeListenerToken import com.amplifyframework.statemachine.codegen.data.AuthChallenge @@ -44,6 +45,8 @@ 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.SignUpState +import com.amplifyframework.statemachine.codegen.states.WebAuthnSignInState import io.mockk.mockk import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -96,6 +99,8 @@ class StateTransitionTests : StateTransitionTestBase() { HostedUISignInState.Resolver(mockHostedUIActions), DeviceSRPSignInState.Resolver(mockDeviceSRPSignInActions), SetupTOTPState.Resolver(mockSetupTOTPActions), + WebAuthnSignInState.Resolver(mockWebAuthnSignInActions, mockSignInActions), + mockUserAuthSignInActions, mockSignInActions ), SignOutState.Resolver(mockSignOutActions), @@ -110,7 +115,8 @@ class StateTransitionTests : StateTransitionTestBase() { DeleteUserState.Resolver(mockDeleteUserActions), mockAuthorizationActions ), - mockAuthActions + mockAuthActions, + SignUpState.Resolver(mockSignUpActions) ), AuthEnvironment(mockk(), configuration, cognitoAuthService, storeClient, null, null, mockk()) ) @@ -283,7 +289,12 @@ class StateTransitionTests : StateTransitionTestBase() { Action { dispatcher, _ -> dispatcher.send( SignInEvent( - SignInEvent.EventType.InitiateSignInWithSRP("username", "password", emptyMap()) + SignInEvent.EventType.InitiateSignInWithSRP( + "username", + "password", + emptyMap(), + AuthFlowType.USER_SRP_AUTH + ) ) ) } @@ -306,7 +317,8 @@ class StateTransitionTests : StateTransitionTestBase() { SignInData.SRPSignInData( "username", "password", - emptyMap() + emptyMap(), + AuthFlowType.USER_SRP_AUTH ) ) ) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/AutoSignInCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/AutoSignInCognitoActionsTest.kt new file mode 100644 index 0000000000..25cc6a89e7 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/AutoSignInCognitoActionsTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2024 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.actions + +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthResponse +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.CognitoUserPoolTokens +import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.SignInData +import com.amplifyframework.statemachine.codegen.data.SignInMethod +import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AutoSignInCognitoActionsTest { + + private val pool = mockk { + every { appClient } returns "client" + every { appClientSecret } returns null + every { pinpointAppId } returns null + } + private val configuration = mockk { + every { userPool } returns pool + } + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk { + coEvery { loadCredentials(CredentialType.ASF) } returns AmplifyCredential.ASFDevice("asf_id") + } + private val logger = mockk(relaxed = true) + private val cognitoIdentityProviderClientMock = mockk() + + private val capturedEvent = slot() + private val dispatcher = mockk { + every { send(capture(capturedEvent)) } just Runs + } + + private lateinit var authEnvironment: AuthEnvironment + + @Before + fun setup() { + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `auto sign in succeeds with signed in state`() = runTest { + val username = "USERNAME" + val userSub = "userId" + val session = "SESSION" + val accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiw" + + "iZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU" + val idToken = "ID_TOKEN" + val refreshToken = "REFRESH_TOKEN" + coEvery { cognitoIdentityProviderClientMock.initiateAuth(any()) } returns InitiateAuthResponse { + this.session = session + this.authenticationResult = AuthenticationResultType.invoke { + this.accessToken = accessToken + this.idToken = idToken + this.refreshToken = refreshToken + this.expiresIn = 100 + } + } + + val signInData = SignInData.AutoSignInData(username, session, mapOf(), userSub) + val initiateAutoSignInEvent = SignInEvent.EventType.InitiateAutoSignIn(signInData) + SignInCognitoActions.autoSignInAction(initiateAutoSignInEvent).execute(dispatcher, authEnvironment) + + val cognitoUserPoolTokens = CognitoUserPoolTokens(idToken, accessToken, refreshToken, 100) + val signInMethod = SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val data = event.eventType.shouldBeInstanceOf().signedInData + + data.username shouldBe username + data.userId shouldBe userSub + data.signInMethod shouldBe signInMethod + data.cognitoUserPoolTokens shouldBe cognitoUserPoolTokens + } + + @Test + fun `auto sign in fails with error when user pool tokens are invalid`() = runTest { + val username = "USERNAME" + val userSub = "123" + val session = "SESSION" + val accessToken = "INVALID_JSON" + coEvery { cognitoIdentityProviderClientMock.initiateAuth(any()) } returns InitiateAuthResponse { + this.session = session + this.authenticationResult = AuthenticationResultType.invoke { + this.accessToken = accessToken + this.idToken = null + this.refreshToken = null + this.expiresIn = 100 + } + } + + val signInData = SignInData.AutoSignInData(username, session, mapOf(), userSub) + val initiateAutoSignInEvent = SignInEvent.EventType.InitiateAutoSignIn(signInData) + SignInCognitoActions.autoSignInAction(initiateAutoSignInEvent).execute(dispatcher, authEnvironment) + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + capturedEvent.captured.type shouldBe expectedEvent.type + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActionsTest.kt new file mode 100644 index 0000000000..eff10369fe --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/MigrateAuthCognitoActionsTest.kt @@ -0,0 +1,299 @@ +/* + * Copyright 2024 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.actions + +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.RespondToAuthChallengeRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.RespondToAuthChallengeResponse +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.SignInMethod +import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MigrateAuthCognitoActionsTest { + private val pool = mockk { + every { appClient } returns "client" + every { appClientSecret } returns null + every { pinpointAppId } returns null + } + private val configuration = mockk { + every { userPool } returns pool + } + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk { + coEvery { loadCredentials(CredentialType.ASF) } returns AmplifyCredential.ASFDevice("asf_id") + } + private val logger = mockk(relaxed = true) + private val cognitoIdentityProviderClientMock = mockk() + + private val capturedEvent = slot() + private val dispatcher = mockk { + every { send(capture(capturedEvent)) } just Runs + } + + private lateinit var authEnvironment: AuthEnvironment + + private val username = "username" + private val password = "password" + private val userId = "1234567890" + private val dummyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g" + + "RG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" + private val dummyIdToken = "id-token" + private val dummyRefreshToken = "refreshToken" + private val bearerToken = "Bearer" + private val dummySession = "session" + private val dummyDeviceKey = "device-key" + private val dummyDeviceGroup = "device-group-key" + + @Before + fun setup() { + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `Initiate USER_AUTH with correct password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = null + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val data = event.eventType.shouldBeInstanceOf().signedInData + + data.userId shouldBe userId + data.username shouldBe username + data.signInMethod shouldBe SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + } + + @Test + fun `Initiate USER_AUTH with incorrect password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.throws(NotAuthorizedException.invoke { }) + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = dummySession, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + event.eventType.shouldBeInstanceOf() + } + + @Test + fun `RespondToAuth USER_AUTH with incorrect password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.throws(NotAuthorizedException.invoke { }) + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = null + ) + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + event.eventType.shouldBeInstanceOf() + } + + @Test + fun `RespondToAuth USER_AUTH with correct password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + RespondToAuthChallengeResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = null + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = "Bearer" + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = null + ) + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val data = event.eventType.shouldBeInstanceOf().signedInData + + data.userId shouldBe userId + data.username shouldBe username + data.signInMethod shouldBe SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_AUTH) + } + + @Test + fun `Initiate USER_PASSWORD_AUTH with correct password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = null + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = password, + authFlowType = AuthFlowType.USER_PASSWORD_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val data = event.eventType.shouldBeInstanceOf().signedInData + + data.userId shouldBe userId + data.username shouldBe username + data.signInMethod shouldBe SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_SRP_AUTH) + } + + @Test + fun `Initiate USER_PASSWORD_AUTH with incorrect password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.throws(NotAuthorizedException.invoke { }) + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + MigrateAuthCognitoActions.initiateMigrateAuthAction( + SignInEvent.EventType.InitiateMigrateAuth( + username = username, + password = dummySession, + authFlowType = AuthFlowType.USER_PASSWORD_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + event.eventType.shouldBeInstanceOf() + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActionsTest.kt new file mode 100644 index 0000000000..b5a00e14ee --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActionsTest.kt @@ -0,0 +1,478 @@ +/* + * Copyright 2024 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.actions + +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.RespondToAuthChallengeRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.RespondToAuthChallengeResponse +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.DeviceMetadata +import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.events.SRPEvent +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlin.test.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SRPCognitoActionsTest { + private val username = "username" + private val password = "password" + private val dummyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g" + + "RG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" + private val dummyIdToken = "id-token" + private val dummyRefreshToken = "refreshToken" + private val bearerToken = "Bearer" + private val dummySession = "session" + private val dummyDeviceKey = "device-key" + private val dummyDeviceGroup = "device-group-key" + + private val pool = mockk { + every { appClient } returns "client" + every { appClientSecret } returns null + every { pinpointAppId } returns null + } + private val configuration = mockk { + every { userPool } returns pool + } + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk { + coEvery { loadCredentials(CredentialType.ASF) } returns AmplifyCredential.ASFDevice("asf_id") + coEvery { loadCredentials(CredentialType.Device(username)) } returns AmplifyCredential.DeviceData( + DeviceMetadata.Metadata( + deviceKey = dummyDeviceKey, + deviceGroupKey = dummyDeviceGroup + ) + ) + } + private val logger = mockk(relaxed = true) + private val cognitoIdentityProviderClientMock = mockk() + + private val capturedEvent = slot() + private val dispatcher = mockk { + every { send(capture(capturedEvent)) }.answers { } + } + + private lateinit var authEnvironment: AuthEnvironment + + private val challengeParams = mapOf( + "SALT" to "salt", + "SECRET_BLOCK" to "secret-block", + "SRP_B" to "srp-b", + "USERNAME" to username, + "USER_ID_FOR_SRP" to username + ) + + @Before + fun setup() { + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `Initiate USER_SRP_AUTH with correct password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = challengeParams + this.challengeName = ChallengeNameType.PasswordVerifier + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = SRPEvent( + SRPEvent.EventType.RespondPasswordVerifier( + challengeParams.plus(mapOf("DEVICE_KEY" to dummyDeviceKey)), + emptyMap(), + dummySession + ) + ) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_SRP_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + dummySession, + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier).session + ) + assertEquals( + challengeParams.plus(mapOf("DEVICE_KEY" to dummyDeviceKey)), + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier) + .challengeParameters + ) + assertEquals( + mapOf("KEY" to "VALUE"), + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier) + .metadata + ) + } + + @Test + fun `Initiate USER_SRP_AUTH with incorrect password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.throws(NotAuthorizedException.invoke { }) + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_SRP_AUTH, + metadata = mapOf("KEY" to "VALUE") + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } + + @Test + fun `RespondToAuth USER_AUTH with correct password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + RespondToAuthChallengeResponse.invoke { + this.session = dummySession + this.challengeParameters = challengeParams + this.challengeName = ChallengeNameType.PasswordVerifier + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = SRPEvent( + SRPEvent.EventType.RespondPasswordVerifier( + challengeParams.plus(mapOf("DEVICE_KEY" to dummyDeviceKey)), + emptyMap(), + dummySession + ) + ) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + dummySession, + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier).session + ) + assertEquals( + challengeParams.plus(mapOf("DEVICE_KEY" to dummyDeviceKey)), + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier) + .challengeParameters + ) + assertEquals( + mapOf("KEY" to "VALUE"), + ((capturedEvent.captured as SRPEvent).eventType as SRPEvent.EventType.RespondPasswordVerifier) + .metadata + ) + } + + @Test + fun `RespondToAuth USER_AUTH with incorrect password`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.throws(NotAuthorizedException.invoke { }) + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } + + @Test + fun `InitAuth response with no challenge params cancels sign in`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = ChallengeNameType.PasswordVerifier + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_SRP_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } + + @Test + fun `RespondToAuth response with no challenge params cancels sign in`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + RespondToAuthChallengeResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = ChallengeNameType.PasswordVerifier + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } + + @Test + fun `InitAuth response with unexpected challenge name cancels sign in`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = ChallengeNameType.SelectChallenge + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_SRP_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } + + @Test + fun `RespondToAuth response with unexpected challenge name cancels sign in`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + RespondToAuthChallengeResponse.invoke { + this.session = dummySession + this.challengeParameters = null + this.challengeName = ChallengeNameType.SelectChallenge + this.authenticationResult { + this.accessToken = dummyToken + this.expiresIn = 3600 + this.idToken = dummyIdToken + this.refreshToken = dummyRefreshToken + this.tokenType = bearerToken + newDeviceMetadata { + this.deviceGroupKey = dummyDeviceGroup + this.deviceKey = dummyDeviceKey + } + } + } + } + + val expectedEvent = AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + + SRPCognitoActions.initiateSRPAuthAction( + SRPEvent.EventType.InitiateSRP( + username = username, + password = password, + authFlowType = AuthFlowType.USER_AUTH, + metadata = mapOf("KEY" to "VALUE"), + respondToAuthChallenge = AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + session = dummySession, + parameters = emptyMap() + ) + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt index 3e1c1bf952..4657b61e17 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt @@ -67,6 +67,9 @@ class SignInChallengeCognitoActionsTest { private lateinit var authEnvironment: AuthEnvironment + private val answer = "myAnswer" + private val username = "fakeUserName" + @Before fun setup() { every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } @@ -84,7 +87,7 @@ class SignInChallengeCognitoActionsTest { @Test fun `very auth challenge without user attributes`() = runTest { val expectedChallengeResponses = mapOf( - "USERNAME" to "testUser" + "USERNAME" to username ) val capturedRequest = slot() coEvery { @@ -94,12 +97,12 @@ class SignInChallengeCognitoActionsTest { } SignInChallengeCognitoActions.verifyChallengeAuthAction( - "myAnswer", + answer, emptyMap(), emptyList(), AuthChallenge( "CONFIRM_SIGN_IN_WITH_NEW_PASSWORD", - username = "testUser", + username = username, session = null, parameters = null ) @@ -113,7 +116,7 @@ class SignInChallengeCognitoActionsTest { fun `user attributes are added to auth challenge`() = runTest { val providedUserAttributes = listOf(AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+15555555555")) val expectedChallengeResponses = mapOf( - "USERNAME" to "testUser", + "USERNAME" to username, "userAttributes.phone_number" to "+15555555555" ) val capturedRequest = slot() @@ -124,12 +127,12 @@ class SignInChallengeCognitoActionsTest { } SignInChallengeCognitoActions.verifyChallengeAuthAction( - "myAnswer", + answer, emptyMap(), providedUserAttributes, AuthChallenge( "CONFIRM_SIGN_IN_WITH_NEW_PASSWORD", - username = "testUser", + username = username, session = null, parameters = null ) @@ -142,7 +145,7 @@ class SignInChallengeCognitoActionsTest { @Test fun `verify email MFA setup selection challenge is handled`() = runTest { val expectedChallengeResponses = mapOf( - "USERNAME" to "testUser", + "USERNAME" to username, ) val capturedRequest = slot() @@ -158,7 +161,98 @@ class SignInChallengeCognitoActionsTest { emptyList(), AuthChallenge( "MFA_SETUP", - username = "testUser", + username = username, + session = null, + parameters = null + ) + ).execute(dispatcher, authEnvironment) + + assertTrue(capturedRequest.isCaptured) + assertEquals(expectedChallengeResponses, capturedRequest.captured.challengeResponses) + } + + @Test + fun `verify challenge response key for Email Otp`() = runTest { + val expectedChallengeResponses = mapOf( + "USERNAME" to username, + "EMAIL_OTP_CODE" to answer + ) + + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + mockk() + } + + SignInChallengeCognitoActions.verifyChallengeAuthAction( + answer, + emptyMap(), + emptyList(), + AuthChallenge( + "EMAIL_OTP", + username = username, + session = null, + parameters = null + ) + ).execute(dispatcher, authEnvironment) + + assertTrue(capturedRequest.isCaptured) + assertEquals(expectedChallengeResponses, capturedRequest.captured.challengeResponses) + } + + @Test + fun `verify challenge response key for SMS Otp`() = runTest { + val expectedChallengeResponses = mapOf( + "USERNAME" to username, + "SMS_OTP_CODE" to answer + ) + + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + mockk() + } + + SignInChallengeCognitoActions.verifyChallengeAuthAction( + answer, + emptyMap(), + emptyList(), + AuthChallenge( + "SMS_OTP", + username = username, + session = null, + parameters = null + ) + ).execute(dispatcher, authEnvironment) + + assertTrue(capturedRequest.isCaptured) + assertEquals(expectedChallengeResponses, capturedRequest.captured.challengeResponses) + } + + @Test + fun `verify challenge response key for Select Challenge`() = runTest { + val selectChallengeChallengeResponseKey = "ANSWER" + val expectedChallengeResponses = mapOf( + "USERNAME" to username, + selectChallengeChallengeResponseKey to answer + ) + + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + mockk() + } + + SignInChallengeCognitoActions.verifyChallengeAuthAction( + answer, + emptyMap(), + emptyList(), + AuthChallenge( + "SELECT_CHALLENGE", + username = username, session = null, parameters = null ) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActionsTest.kt new file mode 100644 index 0000000000..23167ce5b1 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignUpCognitoActionsTest.kt @@ -0,0 +1,294 @@ +/* + * Copyright 2024 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.actions + +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeDeliveryDetailsType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeliveryMediumType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpResponse +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.auth.cognito.usecases.toAuthCodeDeliveryDetails +import com.amplifyframework.auth.result.step.AuthSignUpStep +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.SignUpData +import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration +import com.amplifyframework.statemachine.codegen.events.SignUpEvent +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SignUpCognitoActionsTest { + + private val pool = mockk { + every { appClient } returns "client" + every { appClientSecret } returns null + every { pinpointAppId } returns null + } + private val configuration = mockk { + every { userPool } returns pool + } + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk { + coEvery { loadCredentials(CredentialType.ASF) } returns AmplifyCredential.ASFDevice("asf_id") + } + private val logger = mockk(relaxed = true) + private val cognitoIdentityProviderClientMock = mockk() + + private val capturedEvent = slot() + private val dispatcher = mockk { + every { send(capture(capturedEvent)) } just Runs + } + + private lateinit var authEnvironment: AuthEnvironment + + @Before + fun setup() { + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `sign up succeeds with sign up initiated state`() = runTest { + val session = "SESSION" + val username = "USERNAME" + val userSub = "123" + val codeDeliveryDetails = CodeDeliveryDetailsType.invoke { + this.destination = "DESTINATION" + this.deliveryMedium = DeliveryMediumType.Email + this.attributeName = "ATTRIBUTE" + } + coEvery { cognitoIdentityProviderClientMock.signUp(any()) } returns SignUpResponse { + this.codeDeliveryDetails = codeDeliveryDetails + this.userSub = userSub + this.session = session + this.userConfirmed = false + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, null) + SignUpCognitoActions.initiateSignUpAction( + SignUpEvent.EventType.InitiateSignUp(signUpData) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session shouldBe session + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.CONFIRM_SIGN_UP_STEP + it.nextStep.codeDeliveryDetails shouldBe codeDeliveryDetails.toAuthCodeDeliveryDetails() + it.isSignUpComplete.shouldBeFalse() + it.userId shouldBe userSub + } + } + + @Test + fun `sign up succeeds with confirm sign up state`() = runTest { + val session = "SESSION" + val username = "USERNAME" + val userSub = "123" + val codeDeliveryDetails = CodeDeliveryDetailsType.invoke { + this.destination = "DESTINATION" + this.deliveryMedium = DeliveryMediumType.Email + this.attributeName = "ATTRIBUTE" + } + coEvery { cognitoIdentityProviderClientMock.signUp(any()) } returns SignUpResponse { + this.codeDeliveryDetails = codeDeliveryDetails + this.userSub = userSub + this.session = session + this.userConfirmed = false + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, null) + SignUpCognitoActions.initiateSignUpAction( + SignUpEvent.EventType.InitiateSignUp(signUpData) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session shouldBe session + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.CONFIRM_SIGN_UP_STEP + it.nextStep.codeDeliveryDetails shouldBe codeDeliveryDetails.toAuthCodeDeliveryDetails() + it.isSignUpComplete.shouldBeFalse() + it.userId shouldBe userSub + } + } + + @Test + fun `sign up succeeds with auto sign in state`() = runTest { + val session = "SESSION" + val username = "USERNAME" + val userSub = "123" + coEvery { cognitoIdentityProviderClientMock.signUp(any()) } returns SignUpResponse { + this.userSub = userSub + this.session = session + this.userConfirmed = true + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, null) + SignUpCognitoActions.initiateSignUpAction( + SignUpEvent.EventType.InitiateSignUp(signUpData) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session shouldBe session + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.COMPLETE_AUTO_SIGN_IN + it.isSignUpComplete.shouldBeTrue() + it.userId shouldBe userSub + } + } + + @Test + fun `sign up succeeds with done state`() = runTest { + val username = "USERNAME" + val userSub = "123" + coEvery { cognitoIdentityProviderClientMock.signUp(any()) } returns SignUpResponse { + this.userSub = userSub + this.userConfirmed = true + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, null) + SignUpCognitoActions.initiateSignUpAction( + SignUpEvent.EventType.InitiateSignUp(signUpData) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session.shouldBeNull() + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.DONE + it.isSignUpComplete.shouldBeTrue() + it.userId shouldBe userSub + } + } + + @Test + fun `confirm sign up succeeds with done state`() = runTest { + val username = "USERNAME" + val userSub = "123" + val confirmationCode = "456" + coEvery { cognitoIdentityProviderClientMock.confirmSignUp(any()) } returns ConfirmSignUpResponse { + this.session = null + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, userSub) + SignUpCognitoActions.confirmSignUpAction( + SignUpEvent.EventType.ConfirmSignUp(signUpData, confirmationCode) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session.shouldBeNull() + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.DONE + it.isSignUpComplete.shouldBeTrue() + it.userId shouldBe userSub + } + } + + @Test + fun `confirm sign up succeeds with auto sign in state`() = runTest { + val username = "USERNAME" + val userSub = "123" + val confirmationCode = "456" + val session = "SESSION" + coEvery { cognitoIdentityProviderClientMock.confirmSignUp(any()) } returns ConfirmSignUpResponse { + this.session = session + } + + val signUpData = SignUpData(username, mapOf(), mapOf(), null, userSub) + SignUpCognitoActions.confirmSignUpAction( + SignUpEvent.EventType.ConfirmSignUp(signUpData, confirmationCode) + ).execute(dispatcher, authEnvironment) + + val event = capturedEvent.captured.shouldBeInstanceOf() + val eventType = event.eventType.shouldBeInstanceOf() + + eventType.signUpData.should { + it.username shouldBe username + it.session shouldBe session + it.userId shouldBe userSub + } + + eventType.signUpResult.should { + it.nextStep.signUpStep shouldBe AuthSignUpStep.COMPLETE_AUTO_SIGN_IN + it.isSignUpComplete.shouldBeTrue() + it.userId shouldBe userSub + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActionsTest.kt new file mode 100644 index 0000000000..2e24d85d88 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/UserAuthSignInCognitoActionsTest.kt @@ -0,0 +1,377 @@ +/* + * Copyright 2024 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.actions + +import android.app.Activity +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthResponse +import com.amplifyframework.auth.AuthFactorType +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.CredentialType +import com.amplifyframework.statemachine.codegen.data.DeviceMetadata +import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import java.lang.ref.WeakReference +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UserAuthSignInCognitoActionsTest { + private val username = "USERNAME" + private val dummySession = "session" + private val dummyDeviceKey = "device-key" + private val dummyDeviceGroup = "device-group-key" + + private val pool = mockk { + every { appClient } returns "client" + every { appClientSecret } returns null + every { pinpointAppId } returns null + } + private val configuration = mockk { + every { userPool } returns pool + } + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk { + coEvery { loadCredentials(CredentialType.ASF) } returns AmplifyCredential.ASFDevice("asf_id") + coEvery { loadCredentials(CredentialType.Device(username)) } returns AmplifyCredential.DeviceData( + DeviceMetadata.Metadata( + deviceKey = dummyDeviceKey, + deviceGroupKey = dummyDeviceGroup + ) + ) + } + private val logger = mockk(relaxed = true) + private val cognitoIdentityProviderClientMock = mockk() + + private val capturedEvent = slot() + private val dispatcher = mockk { + every { send(capture(capturedEvent)) }.answers { } + } + + private val callingActivity = mockk() + + private lateinit var authEnvironment: AuthEnvironment + + @Before + fun setup() { + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `Test no preferred preference and receive SELECT_CHALLENGE`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = emptyMap() + this.challengeName = ChallengeNameType.SelectChallenge + this.availableChallenges = listOf( + ChallengeNameType.EmailOtp, + ChallengeNameType.WebAuthn, + ChallengeNameType.Password + ) + } + } + + val availableChallenges = listOf( + ChallengeNameType.EmailOtp.value, + ChallengeNameType.WebAuthn.value, + ChallengeNameType.Password.value + ) + + val expectedEvent = SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.SelectChallenge.value, + username = username, + session = dummySession, + parameters = null, + availableChallenges = availableChallenges + ) + ) + ) + + UserAuthSignInCognitoActions.initiateUserAuthSignIn( + SignInEvent.EventType.InitiateUserAuth( + username = username, + preferredChallenge = null, + callingActivity = WeakReference(callingActivity), + metadata = emptyMap() + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + + assertEquals( + dummySession, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.session + ) + assertEquals( + ChallengeNameType.SelectChallenge.value, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.challengeName + ) + assertEquals( + username, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.username + ) + assertNull( + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.parameters + ) + assertEquals( + availableChallenges, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.availableChallenges + ) + } + + @Test + fun `Test preferred preference of EMAIL_OTP and receive RECEIVED_CHALLENGE`() = runTest { + val challengeParams = mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to "EMAIL", + "CODE_DELIVERY_DESTINATION" to "a***@a***" + ) + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = challengeParams + this.challengeName = ChallengeNameType.EmailOtp + } + } + + val expectedEvent = SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.EmailOtp.value, + username = username, + session = dummySession, + availableChallenges = null, + parameters = challengeParams + ) + ) + ) + + UserAuthSignInCognitoActions.initiateUserAuthSignIn( + SignInEvent.EventType.InitiateUserAuth( + username = username, + preferredChallenge = AuthFactorType.EMAIL_OTP, + callingActivity = WeakReference(callingActivity), + metadata = emptyMap() + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + dummySession, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.session + ) + assertEquals( + ChallengeNameType.EmailOtp.value, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.challengeName + ) + assertEquals( + username, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.username + ) + assertEquals( + challengeParams, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.parameters + ) + assertNull( + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.availableChallenges + ) + } + + @Test + fun `Test preferred preference of SMS_OTP and receive RECEIVED_CHALLENGE`() = runTest { + val challengeParams = mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to "SMS", + "CODE_DELIVERY_DESTINATION" to "a***@a***" + ) + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = challengeParams + this.challengeName = ChallengeNameType.SmsOtp + } + } + + val expectedEvent = SignInEvent( + SignInEvent.EventType.ReceivedChallenge( + AuthChallenge( + challengeName = ChallengeNameType.SmsOtp.value, + username = username, + session = dummySession, + availableChallenges = null, + parameters = challengeParams + ) + ) + ) + + UserAuthSignInCognitoActions.initiateUserAuthSignIn( + SignInEvent.EventType.InitiateUserAuth( + username = username, + preferredChallenge = AuthFactorType.SMS_OTP, + callingActivity = WeakReference(callingActivity), + metadata = emptyMap() + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + dummySession, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.session + ) + assertEquals( + ChallengeNameType.SmsOtp.value, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.challengeName + ) + assertEquals( + username, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.username + ) + assertEquals( + challengeParams, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.parameters + ) + assertNull( + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.ReceivedChallenge) + .challenge.availableChallenges + ) + } + + @Test + fun `Test preferred preference of WEB_AUTHN and receive RECEIVED_CHALLENGE`() = runTest { + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.initiateAuth(capture(capturedRequest)) + }.answers { + InitiateAuthResponse.invoke { + this.session = dummySession + this.challengeParameters = mapOf( + "CREDENTIAL_REQUEST_OPTIONS" to "json" + ) + this.challengeName = ChallengeNameType.WebAuthn + } + } + + val callingActivityReference = WeakReference(callingActivity) + + val expectedEvent = SignInEvent( + SignInEvent.EventType.InitiateWebAuthnSignIn( + WebAuthnSignInContext( + username = username, + callingActivity = callingActivityReference, + session = dummySession, + requestJson = "json" + ) + ) + ) + + UserAuthSignInCognitoActions.initiateUserAuthSignIn( + SignInEvent.EventType.InitiateUserAuth( + username = username, + preferredChallenge = AuthFactorType.WEB_AUTHN, + callingActivity = callingActivityReference, + metadata = emptyMap() + ) + ).execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + dummySession, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.InitiateWebAuthnSignIn) + .signInContext.session + ) + assertEquals( + username, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.InitiateWebAuthnSignIn) + .signInContext.username + ) + assertEquals( + "json", + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.InitiateWebAuthnSignIn) + .signInContext.requestJson + ) + assertEquals( + callingActivityReference, + ((capturedEvent.captured as SignInEvent).eventType as SignInEvent.EventType.InitiateWebAuthnSignIn) + .signInContext.callingActivity + ) + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActionsTest.kt new file mode 100644 index 0000000000..75490b6e72 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/WebAuthnSignInCognitoActionsTest.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2024 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.actions + +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.RespondToAuthChallengeResponse +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.ChallengeParameter +import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent +import com.amplifyframework.statemachine.codegen.events.WebAuthnEvent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.MockKAssertScope +import io.mockk.MockKVerificationScope +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.verify +import java.lang.ref.WeakReference +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class WebAuthnSignInCognitoActionsTest { + + private val signInContext = WebAuthnSignInContext( + username = "username", + callingActivity = WeakReference(null), + session = "session", + requestJson = null, + responseJson = null + ) + + private val identityProviderClient = mockk() + + private val dispatcher = mockk(relaxed = true) + private val authEnvironment = mockk { + every { context } returns mockk() + every { cognitoAuthService.cognitoIdentityProviderClient } returns identityProviderClient + coEvery { getUserContextData(any()) } returns null + every { getPinpointEndpointId() } returns null + every { configuration.userPool?.appClient } returns "app-client" + } + + @Test + fun `fetchCredentialOptions dispatches assert credentials event`() = runTest { + val expectedSignInContext = WebAuthnSignInContext( + username = signInContext.username, + callingActivity = signInContext.callingActivity, + session = "new-session", + requestJson = "request-json", + responseJson = null + ) + + coEvery { identityProviderClient.respondToAuthChallenge(any()) } returns RespondToAuthChallengeResponse { + session = "new-session" + challengeName = ChallengeNameType.WebAuthn + challengeParameters = mapOf( + ChallengeParameter.CredentialRequestOptions.key to "request-json" + ) + } + + val event = WebAuthnEvent.EventType.FetchCredentialOptions(signInContext) + val action = WebAuthnSignInCognitoActions.fetchCredentialOptions(event) + action.execute(dispatcher, authEnvironment) + + verify { + dispatcher.send( + withSignInEvent { + it.signInContext shouldBe expectedSignInContext + } + ) + } + } + + @Test + fun `fetchCredentialsOptions results in InvalidStateException if there is no identity provider client`() = runTest { + every { authEnvironment.cognitoAuthService.cognitoIdentityProviderClient } returns null + + val event = WebAuthnEvent.EventType.FetchCredentialOptions(signInContext) + val action = WebAuthnSignInCognitoActions.fetchCredentialOptions(event) + action.execute(dispatcher, authEnvironment) + + verify { + dispatcher.send( + withWebAuthnEvent { + it.exception.shouldBeInstanceOf() + } + ) + } + } + + @Test + fun `assertCredentials dispatches verify credentials event`() = runTest { + val requestContext = signInContext.copy(requestJson = "request-json") + val expectedSignInContext = requestContext.copy(responseJson = "response-json") + + val event = WebAuthnEvent.EventType.AssertCredentialOptions(requestContext) + val action = WebAuthnSignInCognitoActions.assertCredentials(event) + + mockkConstructor(WebAuthnHelper::class) { + coEvery { anyConstructed().getCredential("request-json", any()) } returns "response-json" + action.execute(dispatcher, authEnvironment) + } + + verify { + dispatcher.send( + withWebAuthnEvent { + it.signInContext shouldBe expectedSignInContext + } + ) + } + } + + @Test + fun `assertCredentials results in InvalidStateException if request JSON is missing`() = runTest { + val requestContext = signInContext.copy(requestJson = null) + val event = WebAuthnEvent.EventType.AssertCredentialOptions(requestContext) + val action = WebAuthnSignInCognitoActions.assertCredentials(event) + + mockkConstructor(WebAuthnHelper::class) { + coEvery { anyConstructed().getCredential("request-json", any()) } returns "response-json" + action.execute(dispatcher, authEnvironment) + } + + verify { + dispatcher.send( + withWebAuthnEvent { + it.exception.shouldBeInstanceOf() + } + ) + } + } + + @Test + fun `verifyCredentials dispatches signedInCompleted event`() = runTest { + val requestContext = signInContext.copy(responseJson = "response-json") + + coEvery { identityProviderClient.respondToAuthChallenge(any()) } returns RespondToAuthChallengeResponse { + authenticationResult = AuthenticationResultType { + } + } + + val event = WebAuthnEvent.EventType.VerifyCredentialsAndSignIn(requestContext) + val action = WebAuthnSignInCognitoActions.verifyCredentialAndSignIn(event) + action.execute(dispatcher, authEnvironment) + + verify { + dispatcher.send(withAuthEvent()) + } + } + + @Test + fun `verifyCredentials results in InvalidStateException if missing response json`() = runTest { + val requestContext = signInContext.copy(responseJson = null) + + coEvery { identityProviderClient.respondToAuthChallenge(any()) } returns RespondToAuthChallengeResponse { + authenticationResult = AuthenticationResultType { + } + } + + val event = WebAuthnEvent.EventType.VerifyCredentialsAndSignIn(requestContext) + val action = WebAuthnSignInCognitoActions.verifyCredentialAndSignIn(event) + action.execute(dispatcher, authEnvironment) + + verify { + dispatcher.send( + withWebAuthnEvent { + it.exception.shouldBeInstanceOf() + } + ) + } + } + + private inline fun MockKVerificationScope.withWebAuthnEvent( + noinline assertions: MockKAssertScope.(T) -> Unit = { } + ) = withArg { + val event = it.shouldBeInstanceOf() + val type = event.eventType.shouldBeInstanceOf() + assertions(type) + } + + private inline fun MockKVerificationScope.withSignInEvent( + noinline assertions: MockKAssertScope.(T) -> Unit = { } + ) = withArg { + val event = it.shouldBeInstanceOf() + val type = event.eventType.shouldBeInstanceOf() + assertions(type) + } + + private inline fun MockKVerificationScope.withAuthEvent( + 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/featuretest/AuthAPI.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/AuthAPI.kt index 53a1acce58..55f2f1e6af 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/AuthAPI.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/AuthAPI.kt @@ -43,6 +43,7 @@ enum class AuthAPI { resendUserAttributeConfirmationCode, resetPassword, signIn, + autoSignIn, signInWithSocialWebUI, signInWithWebUI, signOut, diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt index 9342b46513..840bdb9972 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt @@ -16,6 +16,7 @@ package com.amplifyframework.auth.cognito.featuretest.generators import aws.sdk.kotlin.services.cognitoidentity.model.CognitoIdentityException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProviderException import aws.smithy.kotlin.runtime.time.Instant import com.amplifyframework.auth.AuthException @@ -27,6 +28,7 @@ import com.amplifyframework.auth.cognito.featuretest.serializers.serialize import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.result.AuthSessionResult import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.SignUpState import com.google.gson.Gson import java.io.BufferedWriter import java.io.File @@ -81,7 +83,13 @@ internal fun AuthState.exportJson() { val reverse = result.deserializeToAuthState() val dirName = "states" - val fileName = "${authNState?.javaClass?.simpleName}_${authZState?.javaClass?.simpleName}.json" + val signUpState = authSignUpState?.let { signUpState -> + if (signUpState !is SignUpState.NotStarted) { + "_${signUpState.javaClass.simpleName}" + } + } + val fileName = "${authNState?.javaClass?.simpleName}_${authZState?.javaClass?.simpleName}$signUpState.json" + writeFile(result, dirName, fileName) println("Json exported:\n $result") println("Serialized can be reversed = ${reverse.serialize() == result}") @@ -179,6 +187,7 @@ fun Any?.toJsonElement(): JsonElement { CognitoIdentityProviderExceptionSerializer, this ) + is AuthFlowType -> this.value.toJsonElement() is AuthSessionResult<*> -> toJsonElement() is CognitoIdentityException -> Json.encodeToJsonElement(CognitoIdentityExceptionSerializer, this) else -> gsonBasedSerializer(this) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt index 18e8aac585..66270dc8b8 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt @@ -15,13 +15,18 @@ package com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators +import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.cognito.featuretest.generators.SerializableProvider +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.auth.result.step.AuthNextSignUpStep +import com.amplifyframework.auth.result.step.AuthSignUpStep import com.amplifyframework.statemachine.codegen.data.AWSCredentials import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.AuthChallenge 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.SignUpData import com.amplifyframework.statemachine.codegen.data.SignedInData import com.amplifyframework.statemachine.codegen.data.SignedOutData import com.amplifyframework.statemachine.codegen.states.AuthState @@ -29,6 +34,7 @@ import com.amplifyframework.statemachine.codegen.states.AuthenticationState import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.SignInChallengeState import com.amplifyframework.statemachine.codegen.states.SignInState +import com.amplifyframework.statemachine.codegen.states.SignUpState import java.util.Date /** @@ -48,7 +54,9 @@ object AuthStateJsonGenerator : SerializableProvider { const val expiration: Long = 2342134 const val userId = "userId" - private const val username = "username" + const val username = "username" + const val session = "session-id" + val emptySession = null private val signedInData = SignedInData( userId = userId, @@ -76,12 +84,14 @@ object AuthStateJsonGenerator : SerializableProvider { private val signedInState = AuthState.Configured( AuthenticationState.SignedIn(signedInData, DeviceMetadata.Empty), - AuthorizationState.SessionEstablished(signedInAmplifyCredential) + AuthorizationState.SessionEstablished(signedInAmplifyCredential), + SignUpState.NotStarted() ) private val signedOutState = AuthState.Configured( AuthenticationState.SignedOut(SignedOutData(username)), - AuthorizationState.Configured() + AuthorizationState.Configured(), + SignUpState.NotStarted() ) private val receivedChallengeState = AuthState.Configured( @@ -100,7 +110,85 @@ object AuthStateJsonGenerator : SerializableProvider { ) ) ), - AuthorizationState.SigningIn() + AuthorizationState.SigningIn(), + SignUpState.NotStarted() + ) + + private val passwordlessSignUpAwaitingUserConfirmationState = AuthState.Configured( + AuthenticationState.SignedOut(SignedOutData(username)), + AuthorizationState.Configured(), + SignUpState.AwaitingUserConfirmation( + SignUpData( + username, + null, + null, + session, + "" + ), + AuthSignUpResult( + false, + AuthNextSignUpStep( + AuthSignUpStep.CONFIRM_SIGN_UP_STEP, + emptyMap(), + AuthCodeDeliveryDetails( + "user@domain.com", + AuthCodeDeliveryDetails.DeliveryMedium.EMAIL, + "attributeName" + ) + ), + "" // aligned with mock in CognitoMockFactory + ) + ) + ) + + private val nonPasswordlessSignUpAwaitingUserConfirmationState = AuthState.Configured( + AuthenticationState.SignedOut(SignedOutData(username)), + AuthorizationState.SessionEstablished(signedInAmplifyCredential), + SignUpState.AwaitingUserConfirmation( + SignUpData( + username, + null, + null, + emptySession, + "" + ), + AuthSignUpResult( + false, + AuthNextSignUpStep( + AuthSignUpStep.CONFIRM_SIGN_UP_STEP, + emptyMap(), + AuthCodeDeliveryDetails( + "user@domain.com", + AuthCodeDeliveryDetails.DeliveryMedium.EMAIL, + "attributeName" + ) + ), + "" // aligned with mock in CognitoMockFactory + ) + ) + ) + + private val passwordlessSignedUpState = AuthState.Configured( + AuthenticationState.SignedOut(SignedOutData(username)), + AuthorizationState.SessionEstablished(signedInAmplifyCredential), + SignUpState.SignedUp( + SignUpData( + username, + null, + null, + session, + "" + ), + AuthSignUpResult( + true, + AuthNextSignUpStep( + AuthSignUpStep.COMPLETE_AUTO_SIGN_IN, + emptyMap(), + null + ), + "" // aligned with mock in CognitoMockFactory + ) + ) ) private val receivedCustomChallengeState = AuthState.Configured( @@ -121,8 +209,16 @@ object AuthStateJsonGenerator : SerializableProvider { ) ) ), - AuthorizationState.SigningIn() + AuthorizationState.SigningIn(), + SignUpState.NotStarted() ) - override val serializables: List = listOf(signedInState, signedOutState, receivedChallengeState) + override val serializables: List = listOf( + signedInState, + signedOutState, + receivedChallengeState, + passwordlessSignUpAwaitingUserConfirmationState, + nonPasswordlessSignUpAwaitingUserConfirmationState, + passwordlessSignedUpState + ) } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignInTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignInTestCaseGenerator.kt index f64ad4696b..b7f2c8eb22 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignInTestCaseGenerator.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignInTestCaseGenerator.kt @@ -15,7 +15,10 @@ package com.amplifyframework.auth.cognito.featuretest.generators.testcasegenerators +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException +import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter import com.amplifyframework.auth.cognito.featuretest.API import com.amplifyframework.auth.cognito.featuretest.AuthAPI @@ -32,6 +35,12 @@ import kotlinx.serialization.json.JsonObject object ConfirmSignInTestCaseGenerator : SerializableProvider { private const val challengeCode = "000000" + private const val userId = "userId" + private const val username = "username" + private const val password = "password" + private const val phone = "+12345678900" + private const val email = "test@****.com" + private const val session = "someSession" private val mockedRespondToAuthChallengeResponse = MockResponse( CognitoType.CognitoIdentityProvider, @@ -62,6 +71,45 @@ object ConfirmSignInTestCaseGenerator : SerializableProvider { ).toJsonElement() ) + private val mockedRespondToAuthSrpResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "respondToAuthChallenge", + ResponseType.Success, + mapOf( + "challengeName" to ChallengeNameType.PasswordVerifier.value, + "challengeParameters" to mapOf( + "SALT" to "abc", + "SECRET_BLOCK" to "secretBlock", + "SRP_B" to "def", + "USERNAME" to username, + "USER_ID_FOR_SRP" to userId + ) + ).toJsonElement() + ) + + private fun mockedRespondToAuthEmailOrSmsResponse(challengeNameType: ChallengeNameType): MockResponse { + val (medium, destination) = if (challengeNameType == ChallengeNameType.EmailOtp) { + Pair("EMAIL", email) + } else { + Pair("SMS", phone) + } + + return MockResponse( + CognitoType.CognitoIdentityProvider, + "respondToAuthChallenge", + ResponseType.Success, + mapOf( + "challengeName" to challengeNameType.value, + "session" to session, + "parameters" to JsonObject(emptyMap()), + "challengeParameters" to mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to medium, + "CODE_DELIVERY_DESTINATION" to destination + ) + ).toJsonElement() + ) + } + private val mockedIdentityIdResponse = MockResponse( CognitoType.CognitoIdentity, "getId", @@ -111,6 +159,68 @@ object ConfirmSignInTestCaseGenerator : SerializableProvider { ).toJsonElement() ) + private val mockedRespondToAuthNotAuthorizedException = MockResponse( + CognitoType.CognitoIdentityProvider, + "respondToAuthChallenge", + ResponseType.Failure, + NotAuthorizedException.invoke { + message = "Incorrect username or password." + }.toJsonElement() + ) + + private val mockedRespondToAuthCodeMismatchException = MockResponse( + CognitoType.CognitoIdentityProvider, + "respondToAuthChallenge", + ResponseType.Failure, + CodeMismatchException.invoke { + message = "Confirmation code entered is not correct." + }.toJsonElement() + ) + + private val notAuthorizedExceptionExpectation = ExpectationShapes.Amplify( + AuthAPI.confirmSignIn, + ResponseType.Failure, + com.amplifyframework.auth.exceptions.NotAuthorizedException( + cause = NotAuthorizedException.invoke { + message = "Incorrect username or password." + } + ).toJsonElement(), + ) + + private val codeMismatchExceptionExpectation = ExpectationShapes.Amplify( + AuthAPI.confirmSignIn, + ResponseType.Failure, + CognitoAuthExceptionConverter.lookup( + CodeMismatchException.invoke { + message = "Confirmation code entered is not correct." + }, + "Confirm Sign in failed." + ).toJsonElement() + ) + + private fun mockedConfirmSignInWithOtpExpectation(challengeNameType: ChallengeNameType): ExpectationShapes.Amplify { + val (medium, destination) = if (challengeNameType == ChallengeNameType.EmailOtp) { + Pair("EMAIL", email) + } else { + Pair("SMS", phone) + } + return ExpectationShapes.Amplify( + apiName = AuthAPI.signIn, + responseType = ResponseType.Success, + response = mapOf( + "isSignedIn" to false, + "nextStep" to mapOf( + "signInStep" to "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo" to JsonObject(emptyMap()), + "codeDeliveryDetails" to mapOf( + "destination" to destination, + "deliveryMedium" to medium, + ) + ) + ).toJsonElement() + ) + } + private val baseCase = FeatureTestCase( description = "Test that SignIn with SMS challenge invokes proper cognito request and returns success", preConditions = PreConditions( @@ -189,5 +299,252 @@ object ConfirmSignInTestCaseGenerator : SerializableProvider { ) ) - override val serializables: List = listOf(baseCase, errorCase, successCaseWithSecondaryChallenge) + // SELECT_CHALLENGE > Select Email OTP + private val userAuthSelectEmailOtpChallenge = FeatureTestCase( + description = "Test that selecting the email OTP challenge returns the proper state", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthEmailOrSmsResponse(ChallengeNameType.EmailOtp) + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to AuthFactorType.EMAIL_OTP.challengeResponse + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedConfirmSignInWithOtpExpectation(ChallengeNameType.EmailOtp), + ExpectationShapes.State("SigningIn_EmailOtp.json") + ) + ) + + // Email OTP > Enter Correct Challenge Code + private val userAuthConfirmEmailOtpCodeSucceeds = FeatureTestCase( + description = "Test that entering the correct email OTP code signs the user in", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_EmailOtp.json", + mockedResponses = listOf( + mockedRespondToAuthChallengeResponse, + mockedIdentityIdResponse, + mockedAWSCredentialsResponse, + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to challengeCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // Email OTP > Enter Incorrect Challenge Code + private val userAuthConfirmEmailOtpCodeFails = FeatureTestCase( + description = "Test that entering the incorrect email OTP code fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_EmailOtp.json", + mockedResponses = listOf( + mockedRespondToAuthCodeMismatchException + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to challengeCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + codeMismatchExceptionExpectation + ) + ) + + // SELECT_CHALLENGE > Select SMS OTP + private val userAuthSelectSmsOtpChallenge = FeatureTestCase( + description = "Test that selecting the SMS OTP challenge returns the proper state", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthEmailOrSmsResponse(ChallengeNameType.SmsOtp) + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to AuthFactorType.SMS_OTP.challengeResponse + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedConfirmSignInWithOtpExpectation(ChallengeNameType.SmsOtp), + ExpectationShapes.State("SigningIn_SmsOtp.json") + ) + ) + + // SMS OTP > Enter Correct Challenge Code + private val userAuthConfirmSmsOtpCodeSucceeds = FeatureTestCase( + description = "Test that entering the correct SMS OTP code signs the user in", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SmsOtp.json", + mockedResponses = listOf( + mockedRespondToAuthChallengeResponse, + mockedIdentityIdResponse, + mockedAWSCredentialsResponse, + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to challengeCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // SMS OTP > Enter Incorrect Challenge Code + private val userAuthConfirmSmsOtpCodeFails = FeatureTestCase( + description = "Test that entering the incorrect SMS OTP code fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SmsOtp.json", + mockedResponses = listOf( + mockedRespondToAuthCodeMismatchException + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to challengeCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + codeMismatchExceptionExpectation + ) + ) + + // SELECT_CHALLENGE > Select Password w/Correct Password + private val userAuthSelectPasswordChallengeSucceeds = FeatureTestCase( + description = "Test that selecting the PASSWORD challenge with the correct password succeeds", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthChallengeResponse + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to password + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // SELECT_CHALLENGE > Select Password w/Incorrect Password + private val userAuthSelectPasswordChallengeFails = FeatureTestCase( + description = "Test that selecting the PASSWORD challenge with the incorrect password fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthNotAuthorizedException + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to password + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + notAuthorizedExceptionExpectation + ) + ) + + // SELECT_CHALLENGE > Select Password_SRP w/Correct Password + private val userAuthSelectPasswordSrpChallengeSucceeds = FeatureTestCase( + description = "Test that selecting the PASSWORD_SRP challenge with the correct password succeeds", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthSrpResponse, + mockedRespondToAuthChallengeResponse, + mockedIdentityIdResponse, + mockedAWSCredentialsResponse, + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to password + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // SELECT_CHALLENGE > Select Password_SRP w/Incorrect Password + private val userAuthSelectPasswordSrpChallengeFails = FeatureTestCase( + description = "Test that selecting the PASSWORD_SRP challenge with the incorrect password fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SigningIn_SelectChallenge.json", + mockedResponses = listOf( + mockedRespondToAuthNotAuthorizedException + ) + ), + api = API( + AuthAPI.confirmSignIn, + params = mapOf( + "challengeResponse" to password + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + notAuthorizedExceptionExpectation + ) + ) + + override val serializables: List = listOf( + baseCase, + errorCase, + successCaseWithSecondaryChallenge, + userAuthSelectEmailOtpChallenge, + userAuthConfirmEmailOtpCodeSucceeds, + userAuthConfirmEmailOtpCodeFails, + userAuthSelectSmsOtpChallenge, + userAuthConfirmSmsOtpCodeSucceeds, + userAuthConfirmSmsOtpCodeFails, + userAuthSelectPasswordChallengeSucceeds, + userAuthSelectPasswordChallengeFails, + userAuthSelectPasswordSrpChallengeSucceeds, + userAuthSelectPasswordSrpChallengeFails, + ) } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignUpTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignUpTestCaseGenerator.kt new file mode 100644 index 0000000000..41e1b837ae --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/ConfirmSignUpTestCaseGenerator.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2024 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.featuretest.generators.testcasegenerators + +import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InvalidParameterException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundException +import com.amplifyframework.auth.cognito.featuretest.API +import com.amplifyframework.auth.cognito.featuretest.AuthAPI +import com.amplifyframework.auth.cognito.featuretest.CognitoType +import com.amplifyframework.auth.cognito.featuretest.ExpectationShapes +import com.amplifyframework.auth.cognito.featuretest.FeatureTestCase +import com.amplifyframework.auth.cognito.featuretest.MockResponse +import com.amplifyframework.auth.cognito.featuretest.PreConditions +import com.amplifyframework.auth.cognito.featuretest.ResponseType +import com.amplifyframework.auth.cognito.featuretest.generators.SerializableProvider +import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator +import com.amplifyframework.auth.cognito.featuretest.generators.toJsonElement +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.auth.result.step.AuthNextSignUpStep +import com.amplifyframework.auth.result.step.AuthSignUpStep +import kotlinx.serialization.json.JsonObject + +object ConfirmSignUpTestCaseGenerator : SerializableProvider { + private val username = AuthStateJsonGenerator.username + private val session = AuthStateJsonGenerator.session + private val confirmationCode = "123" + + private val unregisteredUserException = UserNotFoundException.invoke {} + private val invalidUsernameException = InvalidParameterException.invoke {} + private val invalidConfirmationCodeException = CodeMismatchException.invoke {} + + private val expectedPasswordlessCognitoConfirmSignUpRequest = ExpectationShapes.Cognito.CognitoIdentityProvider( + apiName = "confirmSignUp", + request = mapOf( + "clientId" to "testAppClientId", // This should be pulled from configuration + "username" to username, + "confirmationCode" to confirmationCode, + "session" to session // non-null value in passwordless (is set to session stored in previous state) + ).toJsonElement() + ) + + private val expectedNonPasswordlessCognitoConfirmSignUpRequest = ExpectationShapes.Cognito.CognitoIdentityProvider( + apiName = "confirmSignUp", + request = mapOf( + "clientId" to "testAppClientId", // This should be pulled from configuration + "username" to username, + "confirmationCode" to confirmationCode + // the retrieved session from the previous state is null in non-passwordless + ).toJsonElement() + ) + + private val passwordlessConfirmSignUpReturnsCompleteAutoSignIn = FeatureTestCase( + description = "Test that passwordless confirmSignUp returns CompleteAutoSignIn", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured_AwaitingUserConfirmation.json", + mockedResponses = listOf( + MockResponse( + CognitoType.CognitoIdentityProvider, + "confirmSignUp", + ResponseType.Success, + mapOf( + "session" to session + ).toJsonElement() + ) + ) + ), + api = API( + AuthAPI.confirmSignUp, + params = mapOf( + "username" to username, + "confirmationCode" to confirmationCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedPasswordlessCognitoConfirmSignUpRequest, + ExpectationShapes.Amplify( + apiName = AuthAPI.confirmSignUp, + responseType = ResponseType.Success, + response = AuthSignUpResult( + true, + AuthNextSignUpStep( + AuthSignUpStep.COMPLETE_AUTO_SIGN_IN, + emptyMap(), + null + ), + "" // set to userId stored in previous state + ).toJsonElement() + ) + ) + ) + + private val passwordlessConfirmSignUpWithUnregisteredUserReturnsException = FeatureTestCase( + description = "Test that passwordless confirmSignUp with Unregistered User returns Exception", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured_AwaitingUserConfirmation.json", + mockedResponses = listOf( + MockResponse( + CognitoType.CognitoIdentityProvider, + "confirmSignUp", + ResponseType.Failure, + unregisteredUserException.toJsonElement() + ) + ) + ), + api = API( + AuthAPI.confirmSignUp, + params = mapOf( + "username" to username, + "confirmationCode" to confirmationCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedPasswordlessCognitoConfirmSignUpRequest, + ExpectationShapes.Amplify( + apiName = AuthAPI.confirmSignUp, + responseType = ResponseType.Failure, + com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException( + unregisteredUserException + ).toJsonElement() + ) + ) + ) + + private val passwordlessConfirmSignUpWithInvalidUsernameReturnsException = FeatureTestCase( + description = "Test that passwordless confirmSignUp with Invalid Username returns Exception", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured_AwaitingUserConfirmation.json", + mockedResponses = listOf( + MockResponse( + CognitoType.CognitoIdentityProvider, + "confirmSignUp", + ResponseType.Failure, + invalidUsernameException.toJsonElement() + ) + ) + ), + api = API( + AuthAPI.confirmSignUp, + params = mapOf( + "username" to username, + "confirmationCode" to confirmationCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedPasswordlessCognitoConfirmSignUpRequest, + ExpectationShapes.Amplify( + apiName = AuthAPI.confirmSignUp, + responseType = ResponseType.Failure, + com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException( + cause = invalidUsernameException + ).toJsonElement() + ) + ) + ) + + private val passwordlessConfirmSignUpWithInvalidConfirmationCodeReturnsException = FeatureTestCase( + description = "Test that passwordless confirmSignUp with Invalid Confirmation Code returns Exception", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured_AwaitingUserConfirmation.json", + mockedResponses = listOf( + MockResponse( + CognitoType.CognitoIdentityProvider, + "confirmSignUp", + ResponseType.Failure, + invalidConfirmationCodeException.toJsonElement() + ) + ) + ), + api = API( + AuthAPI.confirmSignUp, + params = mapOf( + "username" to username, + "confirmationCode" to confirmationCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedPasswordlessCognitoConfirmSignUpRequest, + ExpectationShapes.Amplify( + apiName = AuthAPI.confirmSignUp, + responseType = ResponseType.Failure, + com.amplifyframework.auth.cognito.exceptions.service.CodeMismatchException( + cause = invalidConfirmationCodeException + ).toJsonElement() + ) + ) + ) + + private val nonpasswordlessConfirmSignUpReturnsDone = FeatureTestCase( + description = "Test that non passwordless confirmSignUp returns Done", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_SessionEstablished_AwaitingUserConfirmation.json", + mockedResponses = listOf( + MockResponse( + CognitoType.CognitoIdentityProvider, + "confirmSignUp", + ResponseType.Success, + emptyMap().toJsonElement() + ) + ) + ), + api = API( + AuthAPI.confirmSignUp, + params = mapOf( + "username" to username, + "confirmationCode" to confirmationCode + ).toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedNonPasswordlessCognitoConfirmSignUpRequest, + ExpectationShapes.Amplify( + apiName = AuthAPI.confirmSignUp, + responseType = ResponseType.Success, + response = AuthSignUpResult( + true, + AuthNextSignUpStep( + AuthSignUpStep.DONE, + emptyMap(), + null + ), + "" // set to userId stored in previous state + ).toJsonElement() + ) + ) + ) + + override val serializables: List = listOf( + passwordlessConfirmSignUpReturnsCompleteAutoSignIn, + passwordlessConfirmSignUpWithUnregisteredUserReturnsException, + passwordlessConfirmSignUpWithInvalidUsernameReturnsException, + passwordlessConfirmSignUpWithInvalidConfirmationCodeReturnsException, + nonpasswordlessConfirmSignUpReturnsDone + ) +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt index 80c48eae0a..5b8cfc1e85 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt @@ -16,7 +16,9 @@ package com.amplifyframework.auth.cognito.featuretest.generators.testcasegenerators import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResourceNotFoundException +import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.cognito.featuretest.API import com.amplifyframework.auth.cognito.featuretest.AuthAPI import com.amplifyframework.auth.cognito.featuretest.CognitoType @@ -29,6 +31,7 @@ import com.amplifyframework.auth.cognito.featuretest.generators.SerializableProv import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator import com.amplifyframework.auth.cognito.featuretest.generators.toJsonElement import com.amplifyframework.auth.cognito.options.AuthFlowType +import com.amplifyframework.auth.exceptions.InvalidStateException import kotlinx.serialization.json.JsonObject object SignInTestCaseGenerator : SerializableProvider { @@ -36,13 +39,15 @@ object SignInTestCaseGenerator : SerializableProvider { private const val username = "username" private const val password = "password" private const val phone = "+12345678900" + private const val email = "test@****.com" + private const val session = "someSession" private val mockedInitiateAuthResponse = MockResponse( CognitoType.CognitoIdentityProvider, "initiateAuth", ResponseType.Success, mapOf( - "challengeName" to ChallengeNameType.PasswordVerifier.toString(), + "challengeName" to ChallengeNameType.PasswordVerifier.value, "challengeParameters" to mapOf( "SALT" to "abc", "SECRET_BLOCK" to "secretBlock", @@ -53,17 +58,70 @@ object SignInTestCaseGenerator : SerializableProvider { ).toJsonElement() ) + private val mockedInitiateAuthPasswordResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "initiateAuth", + ResponseType.Success, + mapOf( + "authenticationResult" to mapOf( + "idToken" to AuthStateJsonGenerator.dummyToken, + "accessToken" to AuthStateJsonGenerator.dummyToken, + "refreshToken" to AuthStateJsonGenerator.dummyToken, + "expiresIn" to 300 + ) + ).toJsonElement() + ) + + private val mockedInitiateAuthSelectChallengeResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "initiateAuth", + ResponseType.Success, + mapOf( + "challengeName" to ChallengeNameType.SelectChallenge.value, + "session" to session, + "parameters" to JsonObject(emptyMap()), + "availableChallenges" to listOf( + ChallengeNameType.Password.value, + ChallengeNameType.WebAuthn.value, + ChallengeNameType.EmailOtp.value + ) + ).toJsonElement() + ) + + private fun mockedInitiateAuthEmailOrSmsResponse(challengeNameType: ChallengeNameType): MockResponse { + val (medium, destination) = if (challengeNameType == ChallengeNameType.EmailOtp) { + Pair("EMAIL", email) + } else { + Pair("SMS", phone) + } + + return MockResponse( + CognitoType.CognitoIdentityProvider, + "initiateAuth", + ResponseType.Success, + mapOf( + "challengeName" to challengeNameType.value, + "session" to session, + "parameters" to JsonObject(emptyMap()), + "challengeParameters" to mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to medium, + "CODE_DELIVERY_DESTINATION" to destination + ) + ).toJsonElement() + ) + } + private val mockedInitiateAuthForCustomAuthWithoutSRPResponse = MockResponse( CognitoType.CognitoIdentityProvider, "initiateAuth", ResponseType.Success, mapOf( - "challengeName" to ChallengeNameType.CustomChallenge.toString(), + "challengeName" to ChallengeNameType.CustomChallenge.value, "challengeParameters" to mapOf( "SALT" to "abc", "SECRET_BLOCK" to "secretBlock", "SRP_B" to "def", - "USERNAME" to username, + "USERNAME" to username ) ).toJsonElement() ) @@ -89,6 +147,31 @@ object SignInTestCaseGenerator : SerializableProvider { ResourceNotFoundException.invoke {}.toJsonElement() ) + private val mockedInitAuthNotAuthorizedException = MockResponse( + CognitoType.CognitoIdentityProvider, + "initiateAuth", + ResponseType.Failure, + NotAuthorizedException.invoke { + message = "Incorrect username or password." + }.toJsonElement() + ) + + private val notAuthorizedExceptionExpectation = ExpectationShapes.Amplify( + AuthAPI.signIn, + ResponseType.Failure, + com.amplifyframework.auth.exceptions.NotAuthorizedException( + cause = NotAuthorizedException.invoke { + message = "Incorrect username or password." + } + ).toJsonElement() + ) + + private val mockedInvalidStateException = ExpectationShapes.Amplify( + AuthAPI.autoSignIn, + ResponseType.Failure, + InvalidStateException().toJsonElement() + ) + private val mockedRespondToAuthChallengeWithDeviceMetadataResponse = MockResponse( CognitoType.CognitoIdentityProvider, "respondToAuthChallenge", @@ -181,7 +264,7 @@ object SignInTestCaseGenerator : SerializableProvider { "isSignedIn" to true, "nextStep" to mapOf( "signInStep" to "DONE", - "additionalInfo" to JsonObject(emptyMap()), + "additionalInfo" to JsonObject(emptyMap()) ) ).toJsonElement() ) @@ -196,12 +279,52 @@ object SignInTestCaseGenerator : SerializableProvider { "additionalInfo" to JsonObject(emptyMap()), "codeDeliveryDetails" to mapOf( "destination" to phone, - "deliveryMedium" to "SMS", + "deliveryMedium" to "SMS" + ) + ) + ).toJsonElement() + ) + + private val mockedSignInSelectChallengeExpectation = ExpectationShapes.Amplify( + apiName = AuthAPI.signIn, + responseType = ResponseType.Success, + response = mapOf( + "isSignedIn" to false, + "nextStep" to mapOf( + "signInStep" to "CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION", + "additionalInfo" to JsonObject(emptyMap()), + "availableFactors" to listOf( + AuthFactorType.PASSWORD.challengeResponse, + AuthFactorType.WEB_AUTHN.challengeResponse, + AuthFactorType.EMAIL_OTP.challengeResponse ) ) ).toJsonElement() ) + private fun mockedConfirmSignInWithOtpExpectation(challengeNameType: ChallengeNameType): ExpectationShapes.Amplify { + val (medium, destination) = if (challengeNameType == ChallengeNameType.EmailOtp) { + Pair("EMAIL", email) + } else { + Pair("SMS", phone) + } + return ExpectationShapes.Amplify( + apiName = AuthAPI.signIn, + responseType = ResponseType.Success, + response = mapOf( + "isSignedIn" to false, + "nextStep" to mapOf( + "signInStep" to "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo" to JsonObject(emptyMap()), + "codeDeliveryDetails" to mapOf( + "destination" to destination, + "deliveryMedium" to medium + ) + ) + ).toJsonElement() + ) + } + private val mockedSignInCustomAuthChallengeExpectation = ExpectationShapes.Amplify( apiName = AuthAPI.signIn, responseType = ResponseType.Success, @@ -236,6 +359,20 @@ object SignInTestCaseGenerator : SerializableProvider { ).toJsonElement() ) + private val expectedCognitoAutoSignInRequest = ExpectationShapes.Cognito.CognitoIdentityProvider( + apiName = "initiateAuth", + request = mapOf( + "authFlow" to aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType.UserAuth, + "clientId" to "testAppClientId", // This should be pulled from configuration + "authParameters" to mapOf( + "USERNAME" to AuthStateJsonGenerator.username, // pulled from loaded SignedUp state + "SECRET_HASH" to "a hash" + ), + "clientMetadata" to emptyMap(), + "session" to AuthStateJsonGenerator.session // pulled from loaded SignedUp state + ).toJsonElement() + ) + private val mockConfirmDeviceResponse = MockResponse( CognitoType.CognitoIdentityProvider, "confirmDevice", @@ -252,7 +389,7 @@ object SignInTestCaseGenerator : SerializableProvider { mockedInitiateAuthResponse, mockedRespondToAuthChallengeResponse, mockedIdentityIdResponse, - mockedAWSCredentialsResponse, + mockedAWSCredentialsResponse ) ), api = API( @@ -269,6 +406,292 @@ object SignInTestCaseGenerator : SerializableProvider { ) ) + // Init USER_AUTH with no preference + private val signInWithUserAuthWithNoPreferenceReturnsSelectChallenge = FeatureTestCase( + description = "Test that USER_AUTH signIn with no preference returns Select Challenge", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthSelectChallengeResponse + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to null + ) + ).toJsonElement() + + ), + validations = listOf( + mockedSignInSelectChallengeExpectation, + ExpectationShapes.State("SigningIn_SelectChallenge.json") + ) + ) + + // Init USER_AUTH with a preference not supported for the user + private val signInWithUserAuthWithUnsupportedPreferenceReturnsSelectChallenge = FeatureTestCase( + description = "Test that USER_AUTH signIn with an unsupported preference returns Select Challenge", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthSelectChallengeResponse + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to AuthFactorType.SMS_OTP.challengeResponse + ) + ).toJsonElement() + + ), + validations = listOf( + mockedSignInSelectChallengeExpectation, + ExpectationShapes.State("SigningIn_SelectChallenge.json") + ) + ) + + // Init USER_AUTH with EMAIL_OTP preference + private val signInWithUserAuthWithEmailOtpPreferenceReturnsVerifyChallenge = FeatureTestCase( + description = "Test that USER_AUTH signIn with EMAIL preference returns Confirm Sign In With OTP", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthEmailOrSmsResponse(ChallengeNameType.EmailOtp) + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to AuthFactorType.EMAIL_OTP.challengeResponse + ) + ).toJsonElement() + + ), + validations = listOf( + mockedConfirmSignInWithOtpExpectation(ChallengeNameType.EmailOtp), + ExpectationShapes.State("SigningIn_EmailOtp.json") + ) + ) + + // Init USER_AUTH with SMS_OTP preference + private val signInWithUserAuthWithSmsOtpPreferenceReturnsVerifyChallenge = FeatureTestCase( + description = "Test that USER_AUTH signIn with SMS preference returns Confirm Sign In With OTP", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthEmailOrSmsResponse(ChallengeNameType.SmsOtp) + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to AuthFactorType.SMS_OTP.challengeResponse + ) + ).toJsonElement() + + ), + validations = listOf( + mockedConfirmSignInWithOtpExpectation(ChallengeNameType.SmsOtp), + ExpectationShapes.State("SigningIn_SmsOtp.json") + ) + ) + + // Init USER_AUTH with PASSWORD_SRP preference with correct password + private val signInWithUserAuthWithPasswordSrpPreferenceSucceeds = FeatureTestCase( + description = "Test that USER_AUTH signIn with PASSWORD_SRP preference succeeds", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthResponse, + mockedRespondToAuthChallengeResponse, + mockedIdentityIdResponse, + mockedAWSCredentialsResponse + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to password + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to AuthFactorType.PASSWORD_SRP.challengeResponse + ) + ).toJsonElement() + + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // Init USER_AUTH with PASSWORD_SRP preference with incorrect password + private val signInWithUserAuthWithPasswordSrpPreferenceFails = FeatureTestCase( + description = "Test that USER_AUTH signIn with PASSWORD_SRP preference fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitAuthNotAuthorizedException + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to password + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to AuthFactorType.PASSWORD_SRP.challengeResponse + ) + ).toJsonElement() + + ), + validations = listOf( + notAuthorizedExceptionExpectation + ) + ) + + // Init USER_AUTH with PASSWORD preference with correct password + private val signInWithUserAuthWithPasswordPreferenceSucceeds = FeatureTestCase( + description = "Test that USER_AUTH signIn with PASSWORD preference succeeds", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitiateAuthPasswordResponse + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to password + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to ChallengeNameType.Password.value + ) + ).toJsonElement() + + ), + validations = listOf( + mockedSignInSuccessExpectation, + ExpectationShapes.State("SignedIn_SessionEstablished.json") + ) + ) + + // Init USER_AUTH with PASSWORD preference with incorrect password + private val signInWithUserAuthWithPasswordPreferenceFails = FeatureTestCase( + description = "Test that USER_AUTH signIn with PASSWORD preference fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedInitAuthNotAuthorizedException + ) + ), + api = API( + AuthAPI.signIn, + params = mapOf( + "username" to username, + "password" to password + ).toJsonElement(), + options = mapOf( + "signInOptions" to + mapOf( + "authFlow" to AuthFlowType.USER_AUTH.toString(), + "preferredFirstFactor" to ChallengeNameType.Password.value + ) + ).toJsonElement() + + ), + validations = listOf( + notAuthorizedExceptionExpectation + ) + ) + + private val autoSignInSucceeds = FeatureTestCase( + description = "Test that autoSignIn invokes proper cognito request and returns DONE", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_SessionEstablished_SignedUp.json", + mockedResponses = listOf( + mockedInitiateAuthPasswordResponse + ) + ), + api = API( + AuthAPI.autoSignIn, + params = emptyMap().toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + expectedCognitoAutoSignInRequest, + mockedSignInSuccessExpectation + ) + ) + + private val autoSignInWithoutConfirmSignUpFails = FeatureTestCase( + description = "Test that autoSignIn without ConfirmSignUp fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_SessionEstablished_AwaitingUserConfirmation.json", + mockedResponses = listOf() + ), + api = API( + AuthAPI.autoSignIn, + params = emptyMap().toJsonElement(), + options = JsonObject(emptyMap()) + ), + validations = listOf( + mockedInvalidStateException + ) + ) + private val signInWhenResourceNotFoundExceptionCase = FeatureTestCase( description = "Test that SRP signIn invokes proper cognito request and returns " + "ResourceNotFoundException but still signs in successfully", @@ -280,7 +703,7 @@ object SignInTestCaseGenerator : SerializableProvider { mockedRespondToAuthChallengeResponseWhenResourceNotFoundException, mockedRespondToAuthChallengeResponse, mockedIdentityIdResponse, - mockedAWSCredentialsResponse, + mockedAWSCredentialsResponse ) ), api = API( @@ -307,7 +730,7 @@ object SignInTestCaseGenerator : SerializableProvider { mockedRespondToAuthChallengeWithDeviceMetadataResponse, mockConfirmDeviceResponse, mockedIdentityIdResponse, - mockedAWSCredentialsResponse, + mockedAWSCredentialsResponse ) ), api = API( @@ -331,7 +754,7 @@ object SignInTestCaseGenerator : SerializableProvider { "SignedOut_Configured.json", mockedResponses = listOf( mockedInitiateAuthResponse, - mockedSMSChallengeResponse, + mockedSMSChallengeResponse ) ), validations = listOf( @@ -347,7 +770,7 @@ object SignInTestCaseGenerator : SerializableProvider { "SigningIn_SigningIn.json", mockedResponses = listOf( mockedInitiateAuthResponse, - mockedSMSChallengeResponse, + mockedSMSChallengeResponse ) ), api = API( @@ -378,7 +801,7 @@ object SignInTestCaseGenerator : SerializableProvider { AuthAPI.signIn, params = mapOf( "username" to username, - "password" to "", + "password" to "" ).toJsonElement(), options = mapOf( "signInOptions" to @@ -407,7 +830,7 @@ object SignInTestCaseGenerator : SerializableProvider { AuthAPI.signIn, params = mapOf( "username" to username, - "password" to "", + "password" to "" ).toJsonElement(), options = mapOf( "signInOptions" to @@ -434,7 +857,7 @@ object SignInTestCaseGenerator : SerializableProvider { AuthAPI.signIn, params = mapOf( "username" to username, - "password" to "", + "password" to "" ).toJsonElement(), options = mapOf( "signInOptions" to mapOf("authFlow" to AuthFlowType.CUSTOM_AUTH_WITH_SRP.toString()) @@ -461,7 +884,7 @@ object SignInTestCaseGenerator : SerializableProvider { AuthAPI.signIn, params = mapOf( "username" to username, - "password" to "", + "password" to "" ).toJsonElement(), options = mapOf( "signInOptions" to mapOf("authFlow" to AuthFlowType.CUSTOM_AUTH_WITH_SRP.toString()) @@ -489,7 +912,7 @@ object SignInTestCaseGenerator : SerializableProvider { AuthAPI.signIn, params = mapOf( "username" to username, - "password" to "", + "password" to "" ).toJsonElement(), options = mapOf( "signInOptions" to mapOf("authFlow" to AuthFlowType.CUSTOM_AUTH_WITH_SRP.toString()) @@ -511,6 +934,16 @@ object SignInTestCaseGenerator : SerializableProvider { customAuthWithSRPWhenResourceNotFoundExceptionCase, customAuthCaseWhenResourceNotFoundExceptionCase, signInWhenResourceNotFoundExceptionCase, - customAuthWithSRPCaseWhenAliasIsUsedToSignIn + customAuthWithSRPCaseWhenAliasIsUsedToSignIn, + signInWithUserAuthWithNoPreferenceReturnsSelectChallenge, + signInWithUserAuthWithUnsupportedPreferenceReturnsSelectChallenge, + signInWithUserAuthWithEmailOtpPreferenceReturnsVerifyChallenge, + signInWithUserAuthWithSmsOtpPreferenceReturnsVerifyChallenge, + signInWithUserAuthWithPasswordSrpPreferenceSucceeds, + signInWithUserAuthWithPasswordSrpPreferenceFails, + signInWithUserAuthWithPasswordPreferenceSucceeds, + signInWithUserAuthWithPasswordPreferenceFails, + autoSignInSucceeds, + autoSignInWithoutConfirmSignUpFails ) } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt index 6a29dee42a..81f71110f5 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt @@ -15,6 +15,8 @@ package com.amplifyframework.auth.cognito.featuretest.generators.testcasegenerators +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InvalidParameterException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.UsernameExistsException import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.cognito.featuretest.API @@ -33,8 +35,11 @@ import com.amplifyframework.auth.result.step.AuthSignUpStep object SignUpTestCaseGenerator : SerializableProvider { private val username = "user" + private val existingUsername = "anExistingUsername" + private val invalidUsername = "anInvalidUsername" private val password = "password" private val email = "user@domain.com" + private val session = "session-id" private val codeDeliveryDetails = mapOf( "destination" to email, @@ -48,18 +53,90 @@ object SignUpTestCaseGenerator : SerializableProvider { "attributeName" to "" ) +// mock responses for non-passwordless flow region starts + private val mockedUnconfirmedSignUpResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Success, + mapOf( + "codeDeliveryDetails" to codeDeliveryDetails + ).toJsonElement() + ) + + private val mockedConfirmedSignUpResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Success, + mapOf( + "codeDeliveryDetails" to emptyCodeDeliveryDetails, + "userConfirmed" to true + ).toJsonElement() + ) +// mock responses for non-passwordless flow region starts + +// mock responses for passwordless flow region starts + private val mockedPasswordlessUnconfirmedSignUpResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Success, + mapOf( + "codeDeliveryDetails" to codeDeliveryDetails, + "session" to session + ).toJsonElement() + ) + + private val mockedPasswordlessConfirmedSignUpResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Success, + mapOf( + "codeDeliveryDetails" to emptyCodeDeliveryDetails, + "session" to session, + "userConfirmed" to true + ).toJsonElement() + ) +// mock responses for passwordless flow region starts + +// mock error responses flow region starts + private val usernameExistsException = UsernameExistsException.invoke {} + private val usernameInvalidException = InvalidParameterException.invoke {} + + private val mockedSignUpWithExistingUsernameResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Failure, + usernameExistsException.toJsonElement() + ) + + private val mockedSignUpWithInvalidUsernameResponse = MockResponse( + CognitoType.CognitoIdentityProvider, + "signUp", + ResponseType.Failure, + usernameInvalidException.toJsonElement() + ) +// mock error responses flow region starts + + private fun expectedCognitoSignUpRequest( + username: String, + password: String? + ) = ExpectationShapes.Cognito.CognitoIdentityProvider( + apiName = "signUp", + // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html] + request = mapOf( + "clientId" to "testAppClientId", // This should be pulled from configuration + "username" to username, + "password" to password, + "userAttributes" to listOf(mapOf("name" to "email", "value" to email)) + ).toJsonElement() + ) + val baseCase = FeatureTestCase( description = "Test that signup invokes proper cognito request and returns success", preConditions = PreConditions( "authconfiguration.json", "SignedOut_Configured.json", mockedResponses = listOf( - MockResponse( - CognitoType.CognitoIdentityProvider, - "signUp", - ResponseType.Success, - mapOf("codeDeliveryDetails" to codeDeliveryDetails).toJsonElement() - ) + mockedUnconfirmedSignUpResponse ) ), api = API( @@ -73,16 +150,7 @@ object SignUpTestCaseGenerator : SerializableProvider { ).toJsonElement() ), validations = listOf( - ExpectationShapes.Cognito.CognitoIdentityProvider( - apiName = "signUp", - // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html] - request = mapOf( - "clientId" to "testAppClientId", // This should be pulled from configuration - "username" to username, - "password" to password, - "userAttributes" to listOf(mapOf("name" to "email", "value" to email)) - ).toJsonElement() - ), + expectedCognitoSignUpRequest(username, password), ExpectationShapes.Amplify( apiName = AuthAPI.signUp, responseType = ResponseType.Success, @@ -97,7 +165,7 @@ object SignUpTestCaseGenerator : SerializableProvider { "attributeName" ) ), - null + "" // aligned with mock in CognitoMockFactory ).toJsonElement() ) ) @@ -107,25 +175,11 @@ object SignUpTestCaseGenerator : SerializableProvider { description = "Sign up finishes if user is confirmed in the first step", preConditions = baseCase.preConditions.copy( mockedResponses = listOf( - MockResponse( - CognitoType.CognitoIdentityProvider, - "signUp", - ResponseType.Success, - mapOf("codeDeliveryDetails" to emptyCodeDeliveryDetails, "userConfirmed" to true).toJsonElement() - ) + mockedConfirmedSignUpResponse ) ), validations = listOf( - ExpectationShapes.Cognito.CognitoIdentityProvider( - apiName = "signUp", - // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html] - request = mapOf( - "clientId" to "testAppClientId", // This should be pulled from configuration - "username" to username, - "password" to password, - "userAttributes" to listOf(mapOf("name" to "email", "value" to email)) - ).toJsonElement() - ), + expectedCognitoSignUpRequest(username, password), ExpectationShapes.Amplify( apiName = AuthAPI.signUp, responseType = ResponseType.Success, @@ -135,13 +189,168 @@ object SignUpTestCaseGenerator : SerializableProvider { AuthNextSignUpStep( AuthSignUpStep.DONE, emptyMap(), - null + AuthCodeDeliveryDetails( + "", + AuthCodeDeliveryDetails.DeliveryMedium.UNKNOWN, + "" + ) + ), + "" // aligned with mock in CognitoMockFactory + ).toJsonElement() + ) + ) + ) + + private val passwordlessUnconfirmedSignUpWithValidUsernameReturnsConfirmSignUpStep = FeatureTestCase( + description = "Test that passwordless uncofirmed signUp with valid username returns ConfirmSignUpStep", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedPasswordlessUnconfirmedSignUpResponse + ) + ), + api = API( + AuthAPI.signUp, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "userAttributes" to mapOf(AuthUserAttributeKey.email().keyString to email) + ).toJsonElement() + ), + validations = listOf( + expectedCognitoSignUpRequest(username, ""), + ExpectationShapes.Amplify( + apiName = AuthAPI.signUp, + responseType = ResponseType.Success, + response = AuthSignUpResult( + false, + AuthNextSignUpStep( + AuthSignUpStep.CONFIRM_SIGN_UP_STEP, + emptyMap(), + AuthCodeDeliveryDetails( + email, + AuthCodeDeliveryDetails.DeliveryMedium.EMAIL, + "attributeName" + ) + ), + "" // aligned with mock in CognitoMockFactory + ).toJsonElement() + ) + ) + ) + + private val passwordlessConfirmedSignUpWithValidUsernameReturnsCompleteAutoSignIn = FeatureTestCase( + description = "Test that passwordless confirmed signUp with valid username returns CompleteAutoSignIn", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedPasswordlessConfirmedSignUpResponse + ) + ), + api = API( + AuthAPI.signUp, + params = mapOf( + "username" to username, + "password" to "" + ).toJsonElement(), + options = mapOf( + "userAttributes" to mapOf(AuthUserAttributeKey.email().keyString to email) + ).toJsonElement() + ), + validations = listOf( + expectedCognitoSignUpRequest(username, ""), + ExpectationShapes.Amplify( + apiName = AuthAPI.signUp, + responseType = ResponseType.Success, + response = AuthSignUpResult( + true, + AuthNextSignUpStep( + AuthSignUpStep.COMPLETE_AUTO_SIGN_IN, + emptyMap(), + AuthCodeDeliveryDetails( + "", + AuthCodeDeliveryDetails.DeliveryMedium.UNKNOWN, + "" + ) ), - null + "" // aligned with mock in CognitoMockFactory + ).toJsonElement() + ) + ) + ) + + private val passwordlessSignUpWithExistingUsernameFails = FeatureTestCase( + description = "Test that passwordless signUp with an existing username fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedSignUpWithExistingUsernameResponse + ) + ), + api = API( + AuthAPI.signUp, + params = mapOf( + "username" to existingUsername, + "password" to "" + ).toJsonElement(), + options = mapOf( + "userAttributes" to mapOf(AuthUserAttributeKey.email().keyString to email) + ).toJsonElement() + ), + validations = listOf( + expectedCognitoSignUpRequest(existingUsername, ""), + ExpectationShapes.Amplify( + AuthAPI.signUp, + ResponseType.Failure, + com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException( + usernameExistsException ).toJsonElement() ) ) ) - override val serializables: List = listOf(baseCase, signupSuccessCase) + private val passwordlessSignUpWithInvalidUsernameFails = FeatureTestCase( + description = "Test that passwordless signUp with an invalid username fails", + preConditions = PreConditions( + "authconfiguration_userauth.json", + "SignedOut_Configured.json", + mockedResponses = listOf( + mockedSignUpWithInvalidUsernameResponse + ) + ), + api = API( + AuthAPI.signUp, + params = mapOf( + "username" to invalidUsername, + "password" to "" + ).toJsonElement(), + options = mapOf( + "userAttributes" to mapOf(AuthUserAttributeKey.email().keyString to email) + ).toJsonElement() + ), + validations = listOf( + expectedCognitoSignUpRequest(invalidUsername, ""), + ExpectationShapes.Amplify( + AuthAPI.signUp, + ResponseType.Failure, + com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterException( + cause = usernameInvalidException + ).toJsonElement() + ) + ) + ) + + override val serializables: List = listOf( + baseCase, + signupSuccessCase, + passwordlessUnconfirmedSignUpWithValidUsernameReturnsConfirmSignUpStep, + passwordlessConfirmedSignUpWithValidUsernameReturnsCompleteAutoSignIn, + passwordlessSignUpWithExistingUsernameFails, + passwordlessSignUpWithInvalidUsernameFails + ) } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthSignUpResultSerializer.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthSignUpResultSerializer.kt new file mode 100644 index 0000000000..e55fb0a0c3 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthSignUpResultSerializer.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.featuretest.serializers + +import com.amplifyframework.auth.result.AuthSignUpResult +import com.google.gson.Gson +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * gson based AuthSignUpResult Serializer as part of AuthStatesSerializer. + * This is needed for java class serialization with kotlinx.serialization library + */ +object AuthSignUpResultSerializer : KSerializer { + private val gson = Gson() + + override val descriptor: SerialDescriptor + get() = buildClassSerialDescriptor("AuthSignUpResult") + + override fun deserialize(decoder: Decoder): AuthSignUpResult { + val jsonString = decoder.decodeString() + return gson.fromJson(jsonString, AuthSignUpResult::class.java) + } + + override fun serialize(encoder: Encoder, value: AuthSignUpResult) { + val jsonString = gson.toJson(value) + encoder.encodeString(jsonString) + } +} 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 d3a454353d..90b782474f 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 @@ -18,9 +18,11 @@ package com.amplifyframework.auth.cognito.featuretest.serializers import com.amplifyframework.auth.cognito.featuretest.serializers.AuthStatesProxy.Companion.format +import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.DeviceMetadata +import com.amplifyframework.statemachine.codegen.data.SignUpData import com.amplifyframework.statemachine.codegen.data.SignedInData import com.amplifyframework.statemachine.codegen.data.SignedOutData import com.amplifyframework.statemachine.codegen.states.AuthState @@ -28,11 +30,11 @@ import com.amplifyframework.statemachine.codegen.states.AuthenticationState import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.SignInChallengeState import com.amplifyframework.statemachine.codegen.states.SignInState +import com.amplifyframework.statemachine.codegen.states.SignUpState import kotlinx.serialization.Contextual import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encodeToString import kotlinx.serialization.encoding.Decoder @@ -51,6 +53,9 @@ internal data class AuthStatesProxy( @SerialName("AuthorizationState") val authZState: AuthorizationState? = null, @Contextual + @SerialName("SignUpState") + val signUpState: SignUpState? = null, + @Contextual @SerialName("SignInState") val signInState: SignInState = SignInState.NotStarted(), @Contextual @@ -61,6 +66,10 @@ internal data class AuthStatesProxy( @Contextual val signedOutData: SignedOutData? = null, @Contextual + val signUpData: SignUpData? = null, + @Serializable(with = AuthSignUpResultSerializer::class) + val signUpResult: AuthSignUpResult? = null, + @Contextual val authChallenge: AuthChallenge? = null, @Contextual val amplifyCredential: AmplifyCredential? = null @@ -68,7 +77,7 @@ internal data class AuthStatesProxy( internal fun toRealAuthState(): T { return when (type) { - "AuthState.Configured" -> AuthState.Configured(authNState, authZState) as T + "AuthState.Configured" -> AuthState.Configured(authNState, authZState, signUpState) as T "AuthenticationState.SignedOut" -> signedOutData?.let { AuthenticationState.SignedOut(it) } as T "AuthenticationState.SignedIn" -> signedInData?.let { AuthenticationState.SignedIn(it, DeviceMetadata.Empty) @@ -78,6 +87,23 @@ internal data class AuthStatesProxy( "AuthorizationState.SessionEstablished" -> amplifyCredential?.let { AuthorizationState.SessionEstablished(it) } as T + "SignUpState.NotStarted" -> SignUpState.NotStarted("") as T + "SignUpState.InitiatingSignUp" -> signUpData?.let { SignUpState.InitiatingSignUp(it) } as T + "SignUpState.ConfirmingSignUp" -> signUpData?.let { SignUpState.ConfirmingSignUp(it) } as T + "SignUpState.AwaitingUserConfirmation" -> { + signUpData?.let { data -> + signUpResult?.let { result -> + SignUpState.AwaitingUserConfirmation(data, result) + } + } as T + } + "SignUpState.SignedUp" -> { + signUpData?.let { data -> + signUpResult?.let { result -> + SignUpState.SignedUp(data, result) + } + } as T + } "AuthorizationState.SigningIn" -> AuthorizationState.SigningIn() as T "SignInState.ResolvingChallenge" -> SignInState.ResolvingChallenge(signInChallengeState) as T "SignInChallengeState.WaitingForAnswer" -> authChallenge?.let { @@ -97,7 +123,8 @@ internal data class AuthStatesProxy( is AuthState.Configured -> AuthStatesProxy( type = "AuthState.Configured", authNState = authState.authNState, - authZState = authState.authZState + authZState = authState.authZState, + signUpState = authState.authSignUpState, ) is AuthState.ConfiguringAuth -> TODO() is AuthState.ConfiguringAuthentication -> TODO() @@ -171,6 +198,9 @@ internal data class AuthStatesProxy( is SignInState.SigningInWithSRP -> TODO() is SignInState.SigningInWithSRPCustom -> TODO() is SignInState.ResolvingTOTPSetup -> TODO() + is SignInState.SigningInWithUserAuth -> TODO() + is SignInState.SigningInWithWebAuthn -> TODO() + is SignInState.AutoSigningIn -> TODO() } } is SignInChallengeState -> { @@ -185,6 +215,32 @@ internal data class AuthStatesProxy( is SignInChallengeState.Error -> TODO() } } + is SignUpState -> { + when (authState) { + is SignUpState.NotStarted -> AuthStatesProxy( + type = "SignUpState.NotStarted" + ) + is SignUpState.AwaitingUserConfirmation -> AuthStatesProxy( + type = "SignUpState.AwaitingUserConfirmation", + signUpData = authState.signUpData, + signUpResult = authState.signUpResult + ) + is SignUpState.ConfirmingSignUp -> AuthStatesProxy( + type = "SignUpState.ConfirmingSignUp", + signUpData = authState.signUpData + ) + is SignUpState.Error -> TODO() + is SignUpState.InitiatingSignUp -> AuthStatesProxy( + type = "SignUpState.InitiatingSignUp", + signUpData = authState.signUpData + ) + is SignUpState.SignedUp -> AuthStatesProxy( + type = "SignUpState.SignedUp", + signUpData = authState.signUpData, + signUpResult = authState.signUpResult + ) + } + } else -> { error(" Cannot convert to proxy!") } @@ -202,6 +258,11 @@ internal data class AuthStatesProxy( contextual(object : KSerializer by AuthStatesSerializer() {}) contextual(object : KSerializer by AuthStatesSerializer() {}) contextual(object : KSerializer by AuthStatesSerializer() {}) + contextual(object : KSerializer by AuthStatesSerializer() {}) + contextual(object : KSerializer by AuthStatesSerializer() {}) + contextual(object : KSerializer by AuthStatesSerializer() {}) + contextual(object : KSerializer by AuthStatesSerializer() {}) + contextual(object : KSerializer by AuthStatesSerializer() {}) } prettyPrint = true } @@ -214,9 +275,7 @@ internal fun String.deserializeToAuthState(): AuthState = format.decodeFromStrin private class AuthStatesSerializer : KSerializer { val serializer = AuthStatesProxy.serializer() - override fun deserialize(decoder: Decoder): T { - return decoder.decodeSerializableValue(serializer).toRealAuthState() - } + override fun deserialize(decoder: Decoder): T = decoder.decodeSerializableValue(serializer).toRealAuthState() override val descriptor: SerialDescriptor = serializer.descriptor diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/CognitoExceptionSerializers.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/CognitoExceptionSerializers.kt index 0d3352e620..73d10938f1 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/CognitoExceptionSerializers.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/CognitoExceptionSerializers.kt @@ -21,8 +21,11 @@ import aws.sdk.kotlin.services.cognitoidentity.model.CognitoIdentityException import aws.sdk.kotlin.services.cognitoidentity.model.TooManyRequestsException import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProviderException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InvalidParameterException import aws.sdk.kotlin.services.cognitoidentityprovider.model.NotAuthorizedException import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResourceNotFoundException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.UsernameExistsException import com.amplifyframework.auth.exceptions.UnknownException import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -46,6 +49,15 @@ private data class CognitoExceptionSurrogate( TooManyRequestsException::class.java.simpleName -> TooManyRequestsException.invoke { message = errorMessage } as T + UsernameExistsException::class.java.simpleName -> UsernameExistsException.invoke { + message = errorMessage + } as T + InvalidParameterException::class.java.simpleName -> InvalidParameterException.invoke { + message = errorMessage + } as T + UserNotFoundException::class.java.simpleName -> UserNotFoundException.invoke { + message = errorMessage + } as T UnknownException::class.java.simpleName -> UnknownException(message = errorMessage ?: "") as T CodeMismatchException::class.java.simpleName -> CodeMismatchException.invoke { message = errorMessage diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt index a55db8319f..c895b907b6 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt @@ -16,11 +16,12 @@ package com.amplifyframework.auth.cognito.helpers import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.cognito.mockAuthNextSignInStep import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.auth.result.AuthSignInResult -import com.amplifyframework.auth.result.step.AuthNextSignInStep import com.amplifyframework.auth.result.step.AuthSignInStep import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData @@ -32,6 +33,7 @@ import kotlin.test.assertNull class SignInChallengeHelperTest { private val username = "username" private val email = "test@testdomain.com" + private val phoneNumber = "+15555555555" // MFA Setup @Test @@ -57,12 +59,9 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, - emptyMap(), - null, - null, - setOf(MFAType.EMAIL, MFAType.TOTP) + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, + allowedMFATypes = setOf(MFAType.EMAIL, MFAType.TOTP) ) ), signInResult @@ -99,15 +98,13 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, - mapOf("MFAS_CAN_SETUP" to "\"SOFTWARE_TOKEN_MFA\""), - null, - TOTPSetupDetails( + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + additionalInfo = mapOf("MFAS_CAN_SETUP" to "\"SOFTWARE_TOKEN_MFA\""), + totpSetupDetails = TOTPSetupDetails( sharedSecret = totpSetupData.secretCode, username = totpSetupData.username - ), - null + ) ) ), signInResult @@ -138,12 +135,8 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP, - emptyMap(), - null, - null, - null + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP ) ), signInResult @@ -175,12 +168,9 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, - emptyMap(), - null, - null, - setOf(MFAType.EMAIL, MFAType.TOTP, MFAType.SMS) + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, + allowedMFATypes = setOf(MFAType.EMAIL, MFAType.TOTP, MFAType.SMS) ) ), signInResult @@ -188,9 +178,9 @@ class SignInChallengeHelperTest { assertNull(errorResult) } - // Email OTP + // Email OTP (User Auth) / MFA @Test - fun `User is asked to confirm an emailed MFA code`() { + fun `User is asked to confirm an email OTP or email MFA code`() { val deliveryDetails = AuthCodeDeliveryDetails(email, AuthCodeDeliveryDetails.DeliveryMedium.EMAIL) var signInResult: AuthSignInResult? = null @@ -217,12 +207,48 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, - emptyMap(), - deliveryDetails, - null, - null + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, + authCodeDeliveryDetails = deliveryDetails + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // SMS OTP (User Auth) + @Test + fun `User is asked to confirm an SMS OTP code`() { + val deliveryDetails = AuthCodeDeliveryDetails(phoneNumber, AuthCodeDeliveryDetails.DeliveryMedium.SMS) + + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "SMS_OTP", + username = username, + session = "session", + parameters = mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to "sms", + "CODE_DELIVERY_DESTINATION" to phoneNumber + ) + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP, + authCodeDeliveryDetails = deliveryDetails ) ), signInResult @@ -254,12 +280,8 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, - emptyMap(), - null, - null, - null + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE ) ), signInResult @@ -267,10 +289,9 @@ class SignInChallengeHelperTest { assertNull(errorResult) } - // SMS + // SMS MFA @Test fun `User is asked to confirm an SMS MFA code`() { - val phoneNumber = "+15555555555" val deliveryDetails = AuthCodeDeliveryDetails(phoneNumber, AuthCodeDeliveryDetails.DeliveryMedium.SMS) var signInResult: AuthSignInResult? = null @@ -297,12 +318,111 @@ class SignInChallengeHelperTest { assertEquals( AuthSignInResult( false, - AuthNextSignInStep( - AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, - emptyMap(), - deliveryDetails, - null, - null + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, + authCodeDeliveryDetails = deliveryDetails + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // Custom Challenge + @Test + fun `User is asked to confirm a custom challenge`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "CUSTOM_CHALLENGE", + username = username, + session = "session", + parameters = null + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // New Password Required + @Test + fun `User is asked to input a new password`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "NEW_PASSWORD_REQUIRED", + username = username, + session = "session", + parameters = null + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // Select Challenge (User Auth) + @Test + fun `User is asked to select an available challenge`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + val availableFactors = listOf(AuthFactorType.EMAIL_OTP, AuthFactorType.SMS_OTP, AuthFactorType.WEB_AUTHN) + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "SELECT_CHALLENGE", + username = username, + session = "session", + parameters = null, + availableChallenges = availableFactors.map { it.challengeResponse } + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + mockAuthNextSignInStep( + authSignInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, + availableFactors = availableFactors.toSet() ) ), signInResult diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelperTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelperTest.kt new file mode 100644 index 0000000000..d293c8b563 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/WebAuthnHelperTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2024 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.helpers + +import android.app.Activity +import android.content.Context +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.PublicKeyCredential +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import java.lang.ref.WeakReference +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class WebAuthnHelperTest { + + private val requestJson = """{"user":{"name":"test"}}""" + private val responseJson = """{"response":"json"}""" + + private val context = mockk() + private val credentialManager = mockk { + coEvery { getCredential(any(), any()) } returns + GetCredentialResponse(PublicKeyCredential(responseJson)) + coEvery { createCredential(any(), any()) } returns CreatePublicKeyCredentialResponse(responseJson) + } + private val helper = WebAuthnHelper( + context = context, + credentialManager = credentialManager + ) + + @Test + fun `gets credential`() = runTest { + val result = helper.getCredential(requestJson, WeakReference(mockk())) + result shouldBe responseJson + } + + @Test + fun `uses activity context if provided`() = runTest { + val activity = mockk() + helper.getCredential(requestJson, WeakReference(activity)) + coVerify { + credentialManager.getCredential(activity, any()) + } + } + + @Test + fun `uses application context if activity is not provided`() = runTest { + helper.getCredential(requestJson, WeakReference(null)) + coVerify { + credentialManager.getCredential(context, any()) + } + } + + @Test + fun `creates credential`() = runTest { + val result = helper.createCredential(requestJson, mockk()) + result shouldBe responseJson + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt new file mode 100644 index 0000000000..1fa152685a --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2024 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.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.CompleteWebAuthnRegistrationResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.StartWebAuthnRegistrationResponse +import aws.smithy.kotlin.runtime.content.Document +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AssociateWebAuthnCredentialUseCaseTest { + private val identityProviderClient: CognitoIdentityProviderClient = mockk { + coEvery { startWebAuthnRegistration(any()) } returns StartWebAuthnRegistrationResponse { + this.credentialCreationOptions = Document(mapOf("a" to Document("b"))) + } + coEvery { completeWebAuthnRegistration(any()) } returns CompleteWebAuthnRegistrationResponse {} + } + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + } + private val webAuthnHelper: WebAuthnHelper = mockk { + coEvery { createCredential(any(), any()) } returns """{"created":"credential"}""" + } + + private val useCase = AssociateWebAuthnCredentialUseCase( + identityProviderClient, + fetchAuthSession, + stateMachine, + webAuthnHelper + ) + + @Test + fun `fails if not in SignedIn state`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute(mockk(), AuthAssociateWebAuthnCredentialsOptions.defaults()) + } + } + + @Test + fun `invokes startWebAuthnRegistration`() = runTest { + useCase.execute(mockk(), AuthAssociateWebAuthnCredentialsOptions.defaults()) + coVerify { + identityProviderClient.startWebAuthnRegistration( + withArg { it.accessToken shouldBe "access token" } + ) + } + } + + @Test + fun `invokes webAuthnHelper with expected JSON`() = runTest { + useCase.execute(mockk(), AuthAssociateWebAuthnCredentialsOptions.defaults()) + coVerify { + webAuthnHelper.createCredential("{\"a\":\"b\"}", any()) + } + } + + @Test + fun `invokes completeWebAuthnRegistration with created credential`() = runTest { + useCase.execute(mockk(), AuthAssociateWebAuthnCredentialsOptions.defaults()) + coVerify { + identityProviderClient.completeWebAuthnRegistration( + withArg { + it.credential shouldBe Document(mapOf("created" to Document("credential"))) + it.accessToken shouldBe "access token" + } + ) + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt new file mode 100644 index 0000000000..6fd9cd95dd --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeleteWebAuthnCredentialResponse +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DeleteWebAuthnCredentialsUseCaseTest { + private val identityProviderClient: CognitoIdentityProviderClient = mockk { + coEvery { deleteWebAuthnCredential(any()) } returns DeleteWebAuthnCredentialResponse { } + } + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + } + private val useCase = DeleteWebAuthnCredentialUseCase( + identityProviderClient, + fetchAuthSession, + stateMachine + ) + + @Test + fun `fails if not in SignedIn state`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute("credentialId", AuthDeleteWebAuthnCredentialOptions.defaults()) + } + } + + @Test + fun `invokes Kotlin SDK`() = runTest { + useCase.execute("credentialId", AuthDeleteWebAuthnCredentialOptions.defaults()) + + coVerify { + identityProviderClient.deleteWebAuthnCredential( + withArg { + it.credentialId shouldBe "credentialId" + it.accessToken shouldBe "access token" + } + ) + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt new file mode 100644 index 0000000000..ba08322bf9 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024 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.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListWebAuthnCredentialsResponse +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.mockWebAuthnCredentialDescription +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldMatchEach +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ListWebAuthnCredentialsUseCaseTest { + + private val identityProviderClient: CognitoIdentityProviderClient = mockk { + coEvery { listWebAuthnCredentials(any()) } returns ListWebAuthnCredentialsResponse { credentials = emptyList() } + } + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + } + private val useCase = ListWebAuthnCredentialsUseCase(identityProviderClient, fetchAuthSession, stateMachine) + + @Test + fun `invokes identity provider service and maps result`() = runTest { + val credentials = listOf( + mockWebAuthnCredentialDescription(friendlyName = "a"), + mockWebAuthnCredentialDescription(friendlyName = "b") + ) + + coEvery { identityProviderClient.listWebAuthnCredentials(any()) } returns ListWebAuthnCredentialsResponse { + this.credentials = credentials + nextToken = "next" + } + + val result = useCase.execute(AuthListWebAuthnCredentialsOptions.defaults()) + + result.nextToken shouldBe "next" + result.credentials shouldMatchEach listOf( + { it.friendlyName shouldBe "a" }, + { it.friendlyName shouldBe "b" } + ) + } + + @Test + fun `sets options in request`() = runTest { + val options = AWSCognitoAuthListWebAuthnCredentialsOptions { + nextToken = "testNext" + maxResults = 34 + } + useCase.execute(options) + + coVerify { + identityProviderClient.listWebAuthnCredentials( + withArg { + it.nextToken shouldBe "testNext" + it.maxResults shouldBe 34 + } + ) + } + } + + @Test + fun `passes null for missing options`() = runTest { + useCase.execute(AuthListWebAuthnCredentialsOptions.defaults()) + + coVerify { + identityProviderClient.listWebAuthnCredentials( + withArg { + it.nextToken.shouldBeNull() + it.maxResults.shouldBeNull() + } + ) + } + } + + @Test + fun `fails if not in SignedIn state`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute(AuthListWebAuthnCredentialsOptions.defaults()) + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/StateMachineTests.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/StateMachineTests.kt index 5afb7a2169..9aca122c5f 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/StateMachineTests.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/StateMachineTests.kt @@ -15,17 +15,19 @@ package com.amplifyframework.statemachine +import app.cash.turbine.test import com.amplifyframework.statemachine.state.Color import com.amplifyframework.statemachine.state.ColorCounter import com.amplifyframework.statemachine.state.Counter import com.amplifyframework.statemachine.state.CounterStateMachine +import com.amplifyframework.testutils.await +import io.kotest.matchers.shouldBe import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before @@ -33,6 +35,7 @@ import org.junit.Test class StateMachineTests { + private val timeout = 1.seconds private val mainThreadSurrogate = newSingleThreadContext("Main thread") @Before @@ -51,24 +54,40 @@ class StateMachineTests { fun testDefaultState() { val testLatch = CountDownLatch(1) val testMachine = CounterStateMachine.logging() - testMachine.getCurrentState { - assertEquals(0, it.value) + testMachine.getCurrentState { state -> + state.value shouldBe 0 testLatch.countDown() } - assertTrue { testLatch.await(5, TimeUnit.SECONDS) } + testLatch.await(timeout) shouldBe true } @Test - fun testBasicReceive() { + fun `test default state suspending`() = runTest { + val testMachine = CounterStateMachine.logging() + val currentState = testMachine.getCurrentState() + currentState.value shouldBe 0 + } + + @Test + fun testBasicReceive() = runTest { val testLatch = CountDownLatch(1) val testMachine = CounterStateMachine.logging() val increment = Counter.Event("1", Counter.Event.EventType.Increment) testMachine.send(increment) - testMachine.getCurrentState { - assertEquals(1, it.value) + testMachine.getCurrentState { state -> + state.value shouldBe 1 testLatch.countDown() } - assertTrue { testLatch.await(5, TimeUnit.SECONDS) } + testLatch.await(timeout) shouldBe true + } + + @Test + fun `test basic receive suspending`() = runTest { + val testMachine = CounterStateMachine.logging() + val increment = Counter.Event("1", Counter.Event.EventType.Increment) + testMachine.send(increment) + val state = testMachine.getCurrentState() + state.value shouldBe 1 } @Test @@ -76,41 +95,65 @@ class StateMachineTests { val testLatch = CountDownLatch(10) val testMachine = CounterStateMachine() val increment = Counter.Event("1", Counter.Event.EventType.Increment) - (1..10) - .map { i -> - testMachine.send(increment) - testMachine.getCurrentState { - assertEquals(it.value, i) - testLatch.countDown() - } + for (i in 1..10) { + testMachine.send(increment) + testMachine.getCurrentState { state -> + state.value shouldBe i + testLatch.countDown() } - assertTrue { testLatch.await(5, TimeUnit.SECONDS) } + } + testLatch.await(timeout) shouldBe true + } + + @Test + fun `test concurrent receive and read suspending`() = runTest { + val testMachine = CounterStateMachine() + val increment = Counter.Event("1", Counter.Event.EventType.Increment) + for (i in 1..10) { + testMachine.send(increment) + val state = testMachine.getCurrentState() + state.value shouldBe i + } + } + + @Test + fun `test collecting state from flow`() = runTest { + val testMachine = CounterStateMachine.logging() + val increment = Counter.Event("1", Counter.Event.EventType.Increment) + + testMachine.state.test { + awaitItem().value shouldBe 0 + testMachine.send(increment) + awaitItem().value shouldBe 1 + testMachine.send(increment) + awaitItem().value shouldBe 2 + } } @Test fun testExecuteEffects() { val action1Latch = CountDownLatch(1) val action2Latch = CountDownLatch(1) - val action1 = com.amplifyframework.statemachine.BasicAction("basic") { _, _ -> + val action1 = BasicAction("basic") { _, _ -> action1Latch.countDown() } - val action2 = com.amplifyframework.statemachine.BasicAction("basic") { _, _ -> + val action2 = BasicAction("basic") { _, _ -> action2Latch.countDown() } val testMachine = CounterStateMachine.logging() val event = Counter.Event("1", Counter.Event.EventType.IncrementAndDoActions(listOf(action1, action2))) testMachine.send(event) - assertTrue { action1Latch.await(5, TimeUnit.SECONDS) } - assertTrue { action2Latch.await(5, TimeUnit.SECONDS) } + action1Latch.await(timeout) shouldBe true + action2Latch.await(timeout) shouldBe true } @Test fun testDispatchFromAction() { val action1Latch = CountDownLatch(1) val action2Latch = CountDownLatch(1) - val action1 = com.amplifyframework.statemachine.BasicAction("basic") { dispatcher, _ -> + val action1 = BasicAction("basic") { dispatcher, _ -> action1Latch.countDown() - val action2 = com.amplifyframework.statemachine.BasicAction("basic") { _, _ -> + val action2 = BasicAction("basic") { _, _ -> action2Latch.countDown() } val event = Counter.Event("2", Counter.Event.EventType.IncrementAndDoActions(listOf(action2))) @@ -120,8 +163,8 @@ class StateMachineTests { val event = Counter.Event("1", Counter.Event.EventType.IncrementAndDoActions(listOf(action1))) testMachine.send(event) - assertTrue { action1Latch.await(5, TimeUnit.SECONDS) } - assertTrue { action2Latch.await(5, TimeUnit.SECONDS) } + action1Latch.await(timeout) shouldBe true + action2Latch.await(timeout) shouldBe true } @Test @@ -129,7 +172,7 @@ class StateMachineTests { val startState = ColorCounter(Color.red, Counter(0), false) val resolvedState = ColorCounter.Resolver().logging().resolve(startState, Color.Event.next) val expected = ColorCounter(Color.green, Counter(0), false) - assertEquals(expected, resolvedState.newState) + resolvedState.newState shouldBe expected } @Test @@ -137,6 +180,6 @@ class StateMachineTests { val startState = ColorCounter(Color.blue, Counter(2), false) val resolvedState = ColorCounter.Resolver().logging().resolve(startState, Color.Event.next) val expected = ColorCounter(Color.yellow, Counter(2), true) - assertEquals(expected, resolvedState.newState) + resolvedState.newState shouldBe expected } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/util/MaskUtilTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/util/MaskUtilTest.kt new file mode 100644 index 0000000000..1047be6aa7 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/util/MaskUtilTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.statemachine.util + +import io.kotest.matchers.shouldBe +import org.junit.Test + +class MaskUtilTest { + + @Test + fun `masks string`() { + val string = "hello world" + string.mask() shouldBe "hell***" + } + + @Test + fun `string of length four is just stars`() { + val string = "1234" + string.mask() shouldBe "***" + } + + @Test + fun `string of length one is just stars`() { + val string = "1" + string.mask() shouldBe "***" + } + + @Test + fun `null string is just stars`() { + val string: String? = null + string.mask() shouldBe "***" + } + + @Test + fun `masks values in map`() { + val map = mapOf("test" to "something", "other" to "otherthing", "short" to "123") + map.mask("test", "short").toString() shouldBe "{test=some***, other=otherthing, short=***}" + } +} diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt index 787c8390a6..9fafeb8a0c 100644 --- a/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt +++ b/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt @@ -21,6 +21,7 @@ import com.amplifyframework.auth.cognito.featuretest.AuthAPI import com.amplifyframework.auth.cognito.featuretest.AuthAPI.resetPassword import com.amplifyframework.auth.cognito.featuretest.AuthAPI.signIn import com.amplifyframework.auth.cognito.featuretest.AuthAPI.signUp +import com.amplifyframework.auth.cognito.helpers.toAuthFactorTypeOrNull import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions @@ -62,6 +63,7 @@ object AuthOptionsFactory { AuthAPI.resendSignUpCode -> AuthResendSignUpCodeOptions.defaults() AuthAPI.resendUserAttributeConfirmationCode -> AuthResendUserAttributeConfirmationCodeOptions.defaults() signIn -> getSignInOptions(optionsData) + AuthAPI.autoSignIn -> null AuthAPI.signInWithSocialWebUI -> AuthWebUISignInOptions.builder().build() AuthAPI.signInWithWebUI -> AuthWebUISignInOptions.builder().build() AuthAPI.signOut -> getSignOutOptions(optionsData) @@ -76,16 +78,20 @@ object AuthOptionsFactory { AuthAPI.getVersion -> TODO() } as T - private fun getSignInOptions(optionsData: JsonObject): AuthSignInOptions { - return if (optionsData.containsKey("signInOptions")) { + private fun getSignInOptions(optionsData: JsonObject): AuthSignInOptions = + if (optionsData.containsKey("signInOptions")) { val authFlowType = AuthFlowType.valueOf( ((optionsData["signInOptions"] as Map)["authFlow"] as JsonPrimitive).content ) - AWSCognitoAuthSignInOptions.builder().authFlowType(authFlowType).build() + val preferredFirstFactor = + ((optionsData["signInOptions"] as Map)["preferredFirstFactor"] as? JsonPrimitive) + ?.content?.toAuthFactorTypeOrNull() + AWSCognitoAuthSignInOptions.builder().authFlowType( + authFlowType + ).preferredFirstFactor(preferredFirstFactor).build() } else { AuthSignInOptions.defaults() } - } private fun getSignUpOptions(optionsData: JsonObject): AuthSignUpOptions = AuthSignUpOptions.builder().userAttributes( diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt index 5f4dc44115..ab50a9b029 100644 --- a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt +++ b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt @@ -25,6 +25,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResul import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeDeliveryDetailsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmDeviceResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeleteUserResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeliveryMediumType import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceType @@ -46,6 +47,7 @@ import com.amplifyframework.auth.cognito.featuretest.serializers.CognitoIdentity import com.amplifyframework.auth.cognito.featuretest.serializers.CognitoIdentityProviderExceptionSerializer import io.mockk.coEvery import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean @@ -77,10 +79,21 @@ class CognitoMockFactory( this.userConfirmed = if (responseObject.containsKey("userConfirmed")) { (responseObject["userConfirmed"] as? JsonPrimitive)?.boolean ?: false } else false + // session will be null in non-user-auth flow + this.session = (responseObject["session"] as? JsonPrimitive)?.content this.userSub = "" } } } + "confirmSignUp" -> { + coEvery { mockCognitoIPClient.confirmSignUp(any()) } coAnswers { + setupError(mockResponse, responseObject) + ConfirmSignUpResponse.invoke { + // session will be null in non-user-auth flow + this.session = (responseObject["session"] as? JsonPrimitive)?.content + } + } + } "initiateAuth" -> { coEvery { mockCognitoIPClient.initiateAuth(any()) } coAnswers { setupError(mockResponse, responseObject) @@ -96,6 +109,12 @@ class CognitoMockFactory( this.authenticationResult = responseObject["authenticationResult"]?.let { parseAuthenticationResult(it as JsonObject) } + + this.availableChallenges = responseObject["availableChallenges"]?.let { + parseAvailableChallenges(it as JsonArray) + } + + this.session = responseObject["session"]?.toString() } } } @@ -231,6 +250,9 @@ class CognitoMockFactory( return params.mapValues { (k, v) -> (v as JsonPrimitive).content } } + private fun parseAvailableChallenges(availableChallenges: JsonArray) = + availableChallenges.map { ChallengeNameType.fromValue(it.toString().replace("\"", "")) } + private fun parseAuthenticationResult(result: JsonObject): AuthenticationResultType { return AuthenticationResultType.invoke { idToken = (result["idToken"] as JsonPrimitive).content diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoRequestFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoRequestFactory.kt index 7a6747c297..c3f87e2f7d 100644 --- a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoRequestFactory.kt +++ b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoRequestFactory.kt @@ -16,7 +16,10 @@ package featureTest.utilities import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthFlowType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ForgotPasswordRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.InitiateAuthRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpRequest import com.amplifyframework.auth.cognito.featuretest.ExpectationShapes import com.amplifyframework.auth.cognito.helpers.AuthHelper @@ -44,6 +47,33 @@ object CognitoRequestFactory { ForgotPasswordRequest.invoke(expectedRequestBuilder) } + "confirmSignUp" -> { + val params = targetApi.request as JsonObject + val expectedRequest: ConfirmSignUpRequest.Builder.() -> Unit = { + clientId = (params["clientId"] as JsonPrimitive).content + username = (params["username"] as JsonPrimitive).content + confirmationCode = (params["confirmationCode"] as JsonPrimitive).content + session = (params["session"] as? JsonPrimitive)?.content + + secretHash = AuthHelper.getSecretHash("", "", "") + } + ConfirmSignUpRequest.invoke(expectedRequest) + } + + "initiateAuth" -> { + val params = targetApi.request as JsonObject + val expectedRequestBuilder: InitiateAuthRequest.Builder.() -> Unit = { + authFlow = AuthFlowType.fromValue((params["authFlow"] as JsonPrimitive).content) + clientId = (params["clientId"] as JsonPrimitive).content + authParameters = + Json.decodeFromJsonElement>(params["authParameters"] as JsonObject) + session = (params["session"] as JsonPrimitive).content + clientMetadata = + Json.decodeFromJsonElement>(params["clientMetadata"] as JsonObject) + } + InitiateAuthRequest.invoke(expectedRequestBuilder) + } + "signUp" -> { val params = targetApi.request as JsonObject val expectedRequest: SignUpRequest.Builder.() -> Unit = { diff --git a/aws-auth-cognito/src/test/resources/feature-test/configuration/authconfiguration_userauth.json b/aws-auth-cognito/src/test/resources/feature-test/configuration/authconfiguration_userauth.json new file mode 100644 index 0000000000..0619d9672d --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/configuration/authconfiguration_userauth.json @@ -0,0 +1,36 @@ +{ + "UserAgent": "aws-amplify-cli/2.0", + "Version": "1.0", + "auth": { + "plugins": { + "awsCognitoAuthPlugin": { + "UserAgent": "aws-amplify/cli", + "Version": "0.1.0", + "IdentityManager": { + "Default": {} + }, + "CredentialsProvider": { + "CognitoIdentity": { + "Default": { + "PoolId": "us-east-1_testIdentityPoolId", + "Region": "us-east-1" + } + } + }, + "CognitoUserPool": { + "Default": { + "PoolId": "us-east-1_testUserPoolId", + "AppClientId": "testAppClientId", + "AppClientSecret": "testAppClientSecret", + "Region": "us-east-1" + } + }, + "Auth": { + "Default": { + "authenticationFlowType": "USER_AUTH" + } + } + } + } + } +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SignedIn_SessionEstablished.json b/aws-auth-cognito/src/test/resources/feature-test/states/SignedIn_SessionEstablished.json index 33dd7b4365..c0ade06825 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/states/SignedIn_SessionEstablished.json +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SignedIn_SessionEstablished.json @@ -45,5 +45,8 @@ "expiration": 2342134 } } + }, + "SignUpState": { + "type": "SignUpState.NotStarted" } } \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured.json b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured.json index 7dcef9586b..b0b2b8774f 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured.json +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured.json @@ -8,5 +8,8 @@ }, "AuthorizationState": { "type": "AuthorizationState.Configured" + }, + "SignUpState": { + "type": "SignUpState.NotStarted" } } \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured_AwaitingUserConfirmation.json b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured_AwaitingUserConfirmation.json new file mode 100644 index 0000000000..94ac0dde4b --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_Configured_AwaitingUserConfirmation.json @@ -0,0 +1,21 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SignedOut", + "signedOutData": { + "lastKnownUsername": "username" + } + }, + "AuthorizationState": { + "type": "AuthorizationState.Configured" + }, + "SignUpState": { + "type": "SignUpState.AwaitingUserConfirmation", + "signUpData": { + "username": "username", + "session": "session-id", + "userId": "" + }, + "signUpResult": "{\"isSignUpComplete\":false,\"nextStep\":{\"signUpStep\":\"CONFIRM_SIGN_UP_STEP\",\"additionalInfo\":{},\"codeDeliveryDetails\":{\"destination\":\"user@domain.com\",\"deliveryMedium\":\"EMAIL\",\"attributeName\":\"attributeName\"}},\"userId\":\"\"}" + } +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_AwaitingUserConfirmation.json b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_AwaitingUserConfirmation.json new file mode 100644 index 0000000000..5231e313d5 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_AwaitingUserConfirmation.json @@ -0,0 +1,45 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SignedOut", + "signedOutData": { + "lastKnownUsername": "username" + } + }, + "AuthorizationState": { + "type": "AuthorizationState.SessionEstablished", + "amplifyCredential": { + "type": "userAndIdentityPool", + "signedInData": { + "userId": "userId", + "username": "username", + "signedInDate": 1707022800000, + "signInMethod": { + "type": "SignInMethod.ApiBased", + "authType": "USER_SRP_AUTH" + }, + "cognitoUserPoolTokens": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 300 + } + }, + "identityId": "someIdentityId", + "credentials": { + "accessKeyId": "someAccessKey", + "secretAccessKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + }, + "SignUpState": { + "type": "SignUpState.AwaitingUserConfirmation", + "signUpData": { + "username": "username", + "userId": "" + }, + "signUpResult": "{\"isSignUpComplete\":false,\"nextStep\":{\"signUpStep\":\"CONFIRM_SIGN_UP_STEP\",\"additionalInfo\":{},\"codeDeliveryDetails\":{\"destination\":\"user@domain.com\",\"deliveryMedium\":\"EMAIL\",\"attributeName\":\"attributeName\"}},\"userId\":\"\"}" + } +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_SignedUp.json b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_SignedUp.json new file mode 100644 index 0000000000..67a379aafe --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SignedOut_SessionEstablished_SignedUp.json @@ -0,0 +1,46 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SignedOut", + "signedOutData": { + "lastKnownUsername": "username" + } + }, + "AuthorizationState": { + "type": "AuthorizationState.SessionEstablished", + "amplifyCredential": { + "type": "userAndIdentityPool", + "signedInData": { + "userId": "userId", + "username": "username", + "signedInDate": 1707022800000, + "signInMethod": { + "type": "SignInMethod.ApiBased", + "authType": "USER_SRP_AUTH" + }, + "cognitoUserPoolTokens": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 300 + } + }, + "identityId": "someIdentityId", + "credentials": { + "accessKeyId": "someAccessKey", + "secretAccessKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + }, + "SignUpState": { + "type": "SignUpState.SignedUp", + "signUpData": { + "username": "username", + "session": "session-id", + "userId": "" + }, + "signUpResult": "{\"isSignUpComplete\":true,\"nextStep\":{\"signUpStep\":\"COMPLETE_AUTO_SIGN_IN\",\"additionalInfo\":{}},\"userId\":\"\"}" + } +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_EmailOtp.json b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_EmailOtp.json new file mode 100644 index 0000000000..4d463f3e37 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_EmailOtp.json @@ -0,0 +1,24 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SigningIn", + "SignInState": { + "type": "SignInState.ResolvingChallenge", + "SignInChallengeState": { + "type": "SignInChallengeState.WaitingForAnswer", + "authChallenge": { + "challengeName": "EMAIL_OTP", + "username": "username", + "session": "someSession", + "parameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "EMAIL", + "CODE_DELIVERY_DESTINATION": "test@****.com" + } + } + } + } + }, + "AuthorizationState": { + "type": "AuthorizationState.SigningIn" + } +} diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SelectChallenge.json b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SelectChallenge.json new file mode 100644 index 0000000000..ffcbf38f96 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SelectChallenge.json @@ -0,0 +1,22 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SigningIn", + "SignInState": { + "type": "SignInState.ResolvingChallenge", + "SignInChallengeState": { + "type": "SignInChallengeState.WaitingForAnswer", + "authChallenge": { + "challengeName": "SELECT_CHALLENGE", + "username": "username", + "session": "someSession", + "parameters" : {}, + "availableChallenges": [ "PASSWORD", "WEB_AUTHN", "EMAIL_OTP" ] + } + } + } + }, + "AuthorizationState": { + "type": "AuthorizationState.SigningIn" + } +} diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SigningIn.json b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SigningIn.json index dace0d38ce..99bf4e5a71 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SigningIn.json +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SigningIn.json @@ -20,5 +20,8 @@ }, "AuthorizationState": { "type": "AuthorizationState.SigningIn" + }, + "SignUpState": { + "type": "SignUpState.NotStarted" } } \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SmsOtp.json b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SmsOtp.json new file mode 100644 index 0000000000..365d9d853b --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/states/SigningIn_SmsOtp.json @@ -0,0 +1,24 @@ +{ + "type": "AuthState.Configured", + "AuthenticationState": { + "type": "AuthenticationState.SigningIn", + "SignInState": { + "type": "SignInState.ResolvingChallenge", + "SignInChallengeState": { + "type": "SignInChallengeState.WaitingForAnswer", + "authChallenge": { + "challengeName": "SMS_OTP", + "username": "username", + "session": "someSession", + "parameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "SMS", + "CODE_DELIVERY_DESTINATION": "+12345678900" + } + } + } + } + }, + "AuthorizationState": { + "type": "AuthorizationState.SigningIn" + } +} diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_invokes_proper_cognito_request_and_returns_DONE.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_invokes_proper_cognito_request_and_returns_DONE.json new file mode 100644 index 0000000000..67aaba8ead --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_invokes_proper_cognito_request_and_returns_DONE.json @@ -0,0 +1,55 @@ +{ + "description": "Test that autoSignIn invokes proper cognito request and returns DONE", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_SessionEstablished_SignedUp.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + } + ] + }, + "api": { + "name": "autoSignIn", + "params": {}, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "request": { + "authFlow": "USER_AUTH", + "clientId": "testAppClientId", + "authParameters": { + "USERNAME": "username", + "SECRET_HASH": "a hash" + }, + "clientMetadata": {}, + "session": "session-id" + } + }, + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_without_ConfirmSignUp_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_without_ConfirmSignUp_fails.json new file mode 100644 index 0000000000..6cc42fedd8 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/autoSignIn/Test_that_autoSignIn_without_ConfirmSignUp_fails.json @@ -0,0 +1,26 @@ +{ + "description": "Test that autoSignIn without ConfirmSignUp fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_SessionEstablished_AwaitingUserConfirmation.json", + "mockedResponses": [] + }, + "api": { + "name": "autoSignIn", + "params": {}, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "autoSignIn", + "responseType": "failure", + "response": { + "errorType": "InvalidStateException", + "errorMessage": "Auth state is an invalid state, cannot process the request.", + "recoverySuggestion": "Operation performed is not a valid operation for the current auth state.", + "cause": null + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_SMS_OTP_code_signs_the_user_in.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_SMS_OTP_code_signs_the_user_in.json new file mode 100644 index 0000000000..924dc954cd --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_SMS_OTP_code_signs_the_user_in.json @@ -0,0 +1,68 @@ +{ + "description": "Test that entering the correct SMS OTP code signs the user in", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SmsOtp.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + }, + { + "type": "cognitoIdentity", + "apiName": "getId", + "responseType": "success", + "response": { + "identityId": "someIdentityId" + } + }, + { + "type": "cognitoIdentity", + "apiName": "getCredentialsForIdentity", + "responseType": "success", + "response": { + "credentials": { + "accessKeyId": "someAccessKey", + "secretKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "000000" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_email_OTP_code_signs_the_user_in.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_email_OTP_code_signs_the_user_in.json new file mode 100644 index 0000000000..a8760d9176 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_correct_email_OTP_code_signs_the_user_in.json @@ -0,0 +1,68 @@ +{ + "description": "Test that entering the correct email OTP code signs the user in", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_EmailOtp.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + }, + { + "type": "cognitoIdentity", + "apiName": "getId", + "responseType": "success", + "response": { + "identityId": "someIdentityId" + } + }, + { + "type": "cognitoIdentity", + "apiName": "getCredentialsForIdentity", + "responseType": "success", + "response": { + "credentials": { + "accessKeyId": "someAccessKey", + "secretKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "000000" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_SMS_OTP_code_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_SMS_OTP_code_fails.json new file mode 100644 index 0000000000..9f6a5fbc75 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_SMS_OTP_code_fails.json @@ -0,0 +1,41 @@ +{ + "description": "Test that entering the incorrect SMS OTP code fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SmsOtp.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct." + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "000000" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct.", + "recoverySuggestion": "Enter correct confirmation code.", + "cause": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_email_OTP_code_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_email_OTP_code_fails.json new file mode 100644 index 0000000000..0253028539 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_entering_the_incorrect_email_OTP_code_fails.json @@ -0,0 +1,41 @@ +{ + "description": "Test that entering the incorrect email OTP code fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_EmailOtp.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct." + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "000000" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct.", + "recoverySuggestion": "Enter correct confirmation code.", + "cause": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_correct_password_succeeds.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_correct_password_succeeds.json new file mode 100644 index 0000000000..2415a288e1 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_correct_password_succeeds.json @@ -0,0 +1,83 @@ +{ + "description": "Test that selecting the PASSWORD_SRP challenge with the correct password succeeds", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "challengeName": "PASSWORD_VERIFIER", + "challengeParameters": { + "SALT": "abc", + "SECRET_BLOCK": "secretBlock", + "SRP_B": "def", + "USERNAME": "username", + "USER_ID_FOR_SRP": "userId" + } + } + }, + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + }, + { + "type": "cognitoIdentity", + "apiName": "getId", + "responseType": "success", + "response": { + "identityId": "someIdentityId" + } + }, + { + "type": "cognitoIdentity", + "apiName": "getCredentialsForIdentity", + "responseType": "success", + "response": { + "credentials": { + "accessKeyId": "someAccessKey", + "secretKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "password" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_incorrect_password_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_incorrect_password_fails.json new file mode 100644 index 0000000000..0351f29745 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_SRP_challenge_with_the_incorrect_password_fails.json @@ -0,0 +1,41 @@ +{ + "description": "Test that selecting the PASSWORD_SRP challenge with the incorrect password fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "password" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Failed since user is not authorized.", + "recoverySuggestion": "Check whether the given values are correct and the user is authorized to perform the operation.", + "cause": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_correct_password_succeeds.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_correct_password_succeeds.json new file mode 100644 index 0000000000..833cc68207 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_correct_password_succeeds.json @@ -0,0 +1,47 @@ +{ + "description": "Test that selecting the PASSWORD challenge with the correct password succeeds", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "password" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_incorrect_password_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_incorrect_password_fails.json new file mode 100644 index 0000000000..20b337d54d --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_PASSWORD_challenge_with_the_incorrect_password_fails.json @@ -0,0 +1,41 @@ +{ + "description": "Test that selecting the PASSWORD challenge with the incorrect password fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "password" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "confirmSignIn", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Failed since user is not authorized.", + "recoverySuggestion": "Check whether the given values are correct and the user is authorized to perform the operation.", + "cause": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_SMS_OTP_challenge_returns_the_proper_state.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_SMS_OTP_challenge_returns_the_proper_state.json new file mode 100644 index 0000000000..16aed244de --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_SMS_OTP_challenge_returns_the_proper_state.json @@ -0,0 +1,52 @@ +{ + "description": "Test that selecting the SMS OTP challenge returns the proper state", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "challengeName": "SMS_OTP", + "session": "someSession", + "parameters": {}, + "challengeParameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "SMS", + "CODE_DELIVERY_DESTINATION": "+12345678900" + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "SMS_OTP" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "+12345678900", + "deliveryMedium": "SMS" + } + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_SmsOtp.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_email_OTP_challenge_returns_the_proper_state.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_email_OTP_challenge_returns_the_proper_state.json new file mode 100644 index 0000000000..5a164403bb --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignIn/Test_that_selecting_the_email_OTP_challenge_returns_the_proper_state.json @@ -0,0 +1,52 @@ +{ + "description": "Test that selecting the email OTP challenge returns the proper state", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SigningIn_SelectChallenge.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "challengeName": "EMAIL_OTP", + "session": "someSession", + "parameters": {}, + "challengeParameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "EMAIL", + "CODE_DELIVERY_DESTINATION": "test@****.com" + } + } + } + ] + }, + "api": { + "name": "confirmSignIn", + "params": { + "challengeResponse": "EMAIL_OTP" + }, + "options": {} + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "test@****.com", + "deliveryMedium": "EMAIL" + } + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_EmailOtp.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_non_passwordless_confirmSignUp_returns_Done.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_non_passwordless_confirmSignUp_returns_Done.json new file mode 100644 index 0000000000..b0377ada61 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_non_passwordless_confirmSignUp_returns_Done.json @@ -0,0 +1,47 @@ +{ + "description": "Test that non passwordless confirmSignUp returns Done", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_SessionEstablished_AwaitingUserConfirmation.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "responseType": "success", + "response": {} + } + ] + }, + "api": { + "name": "confirmSignUp", + "params": { + "username": "username", + "confirmationCode": "123" + }, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "request": { + "clientId": "testAppClientId", + "username": "username", + "confirmationCode": "123" + } + }, + { + "type": "amplify", + "apiName": "confirmSignUp", + "responseType": "success", + "response": { + "isSignUpComplete": true, + "nextStep": { + "signUpStep": "DONE", + "additionalInfo": {} + }, + "userId": "" + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_returns_CompleteAutoSignIn.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_returns_CompleteAutoSignIn.json new file mode 100644 index 0000000000..5f664a01ee --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_returns_CompleteAutoSignIn.json @@ -0,0 +1,50 @@ +{ + "description": "Test that passwordless confirmSignUp returns CompleteAutoSignIn", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured_AwaitingUserConfirmation.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "responseType": "success", + "response": { + "session": "session-id" + } + } + ] + }, + "api": { + "name": "confirmSignUp", + "params": { + "username": "username", + "confirmationCode": "123" + }, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "request": { + "clientId": "testAppClientId", + "username": "username", + "confirmationCode": "123", + "session": "session-id" + } + }, + { + "type": "amplify", + "apiName": "confirmSignUp", + "responseType": "success", + "response": { + "isSignUpComplete": true, + "nextStep": { + "signUpStep": "COMPLETE_AUTO_SIGN_IN", + "additionalInfo": {} + }, + "userId": "" + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Confirmation_Code_returns_Exception.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Confirmation_Code_returns_Exception.json new file mode 100644 index 0000000000..e2d3d7b20d --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Confirmation_Code_returns_Exception.json @@ -0,0 +1,52 @@ +{ + "description": "Test that passwordless confirmSignUp with Invalid Confirmation Code returns Exception", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured_AwaitingUserConfirmation.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + ] + }, + "api": { + "name": "confirmSignUp", + "params": { + "username": "username", + "confirmationCode": "123" + }, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "request": { + "clientId": "testAppClientId", + "username": "username", + "confirmationCode": "123", + "session": "session-id" + } + }, + { + "type": "amplify", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "CodeMismatchException", + "errorMessage": "Confirmation code entered is not correct.", + "recoverySuggestion": "Enter correct confirmation code.", + "cause": { + "errorType": "CodeMismatchException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Username_returns_Exception.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Username_returns_Exception.json new file mode 100644 index 0000000000..bf6e18898d --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Invalid_Username_returns_Exception.json @@ -0,0 +1,52 @@ +{ + "description": "Test that passwordless confirmSignUp with Invalid Username returns Exception", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured_AwaitingUserConfirmation.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "InvalidParameterException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + ] + }, + "api": { + "name": "confirmSignUp", + "params": { + "username": "username", + "confirmationCode": "123" + }, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "request": { + "clientId": "testAppClientId", + "username": "username", + "confirmationCode": "123", + "session": "session-id" + } + }, + { + "type": "amplify", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "InvalidParameterException", + "errorMessage": "One or more parameters are incorrect.", + "recoverySuggestion": "Enter correct parameters.", + "cause": { + "errorType": "InvalidParameterException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Unregistered_User_returns_Exception.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Unregistered_User_returns_Exception.json new file mode 100644 index 0000000000..d1fdf82dc5 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/confirmSignUp/Test_that_passwordless_confirmSignUp_with_Unregistered_User_returns_Exception.json @@ -0,0 +1,52 @@ +{ + "description": "Test that passwordless confirmSignUp with Unregistered User returns Exception", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured_AwaitingUserConfirmation.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "UserNotFoundException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + ] + }, + "api": { + "name": "confirmSignUp", + "params": { + "username": "username", + "confirmationCode": "123" + }, + "options": {} + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "confirmSignUp", + "request": { + "clientId": "testAppClientId", + "username": "username", + "confirmationCode": "123", + "session": "session-id" + } + }, + { + "type": "amplify", + "apiName": "confirmSignUp", + "responseType": "failure", + "response": { + "errorType": "UserNotFoundException", + "errorMessage": "User not found in the system.", + "recoverySuggestion": "Please enter correct username.", + "cause": { + "errorType": "UserNotFoundException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_invokes_proper_cognito_request_and_returns_success.json new file mode 100644 index 0000000000..8e26d7fba7 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_invokes_proper_cognito_request_and_returns_success.json @@ -0,0 +1,60 @@ +{ + "description": "Test that USER_AUTH signIn invokes proper cognito request and returns success", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "SELECT_CHALLENGE", + "session": "session-id", + "parameters": {}, + "availableChallenges": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": null + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION", + "additionalInfo": {}, + "availableFactors": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_SelectChallenge.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_EMAIL_preference_returns_Confirm_Sign_In_With_OTP.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_EMAIL_preference_returns_Confirm_Sign_In_With_OTP.json new file mode 100644 index 0000000000..d1b61479ed --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_EMAIL_preference_returns_Confirm_Sign_In_With_OTP.json @@ -0,0 +1,58 @@ +{ + "description": "Test that USER_AUTH signIn with EMAIL preference returns Confirm Sign In With OTP", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "EMAIL_OTP", + "session": "someSession", + "parameters": {}, + "challengeParameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "EMAIL", + "CODE_DELIVERY_DESTINATION": "test@****.com" + } + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "EMAIL_OTP" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "test@****.com", + "deliveryMedium": "EMAIL" + } + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_EmailOtp.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_fails.json new file mode 100644 index 0000000000..23328eccc4 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_fails.json @@ -0,0 +1,47 @@ +{ + "description": "Test that USER_AUTH signIn with PASSWORD_SRP preference fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "password" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "PASSWORD_SRP" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Failed since user is not authorized.", + "recoverySuggestion": "Check whether the given values are correct and the user is authorized to perform the operation.", + "cause": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_succeeds.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_succeeds.json new file mode 100644 index 0000000000..2c28e8ba0b --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_SRP_preference_succeeds.json @@ -0,0 +1,89 @@ +{ + "description": "Test that USER_AUTH signIn with PASSWORD_SRP preference succeeds", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "PASSWORD_VERIFIER", + "challengeParameters": { + "SALT": "abc", + "SECRET_BLOCK": "secretBlock", + "SRP_B": "def", + "USERNAME": "username", + "USER_ID_FOR_SRP": "userId" + } + } + }, + { + "type": "cognitoIdentityProvider", + "apiName": "respondToAuthChallenge", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + }, + { + "type": "cognitoIdentity", + "apiName": "getId", + "responseType": "success", + "response": { + "identityId": "someIdentityId" + } + }, + { + "type": "cognitoIdentity", + "apiName": "getCredentialsForIdentity", + "responseType": "success", + "response": { + "credentials": { + "accessKeyId": "someAccessKey", + "secretKey": "someSecretKey", + "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiration": 2342134 + } + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "password" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "PASSWORD_SRP" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_fails.json new file mode 100644 index 0000000000..52fdb8f5f7 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_fails.json @@ -0,0 +1,47 @@ +{ + "description": "Test that USER_AUTH signIn with PASSWORD preference fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "failure", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "password" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "PASSWORD" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "errorType": "NotAuthorizedException", + "errorMessage": "Failed since user is not authorized.", + "recoverySuggestion": "Check whether the given values are correct and the user is authorized to perform the operation.", + "cause": { + "errorType": "NotAuthorizedException", + "errorMessage": "Incorrect username or password." + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_succeeds.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_succeeds.json new file mode 100644 index 0000000000..796c90ca8f --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_PASSWORD_preference_succeeds.json @@ -0,0 +1,53 @@ +{ + "description": "Test that USER_AUTH signIn with PASSWORD preference succeeds", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "authenticationResult": { + "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU", + "expiresIn": 300 + } + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "password" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "PASSWORD" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": true, + "nextStep": { + "signInStep": "DONE", + "additionalInfo": {} + } + } + }, + { + "type": "state", + "expectedState": "SignedIn_SessionEstablished.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_SMS_preference_returns_Confirm_Sign_In_With_OTP.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_SMS_preference_returns_Confirm_Sign_In_With_OTP.json new file mode 100644 index 0000000000..a9ef2f0611 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_SMS_preference_returns_Confirm_Sign_In_With_OTP.json @@ -0,0 +1,58 @@ +{ + "description": "Test that USER_AUTH signIn with SMS preference returns Confirm Sign In With OTP", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "SMS_OTP", + "session": "someSession", + "parameters": {}, + "challengeParameters": { + "CODE_DELIVERY_DELIVERY_MEDIUM": "SMS", + "CODE_DELIVERY_DESTINATION": "+12345678900" + } + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "SMS_OTP" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONFIRM_SIGN_IN_WITH_OTP", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "+12345678900", + "deliveryMedium": "SMS" + } + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_SmsOtp.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_an_unsupported_preference_returns_Select_Challenge.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_an_unsupported_preference_returns_Select_Challenge.json new file mode 100644 index 0000000000..e059642676 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_an_unsupported_preference_returns_Select_Challenge.json @@ -0,0 +1,60 @@ +{ + "description": "Test that USER_AUTH signIn with an unsupported preference returns Select Challenge", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "SELECT_CHALLENGE", + "session": "someSession", + "parameters": {}, + "availableChallenges": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": "SMS_OTP" + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION", + "additionalInfo": {}, + "availableFactors": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_SelectChallenge.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_no_preference_returns_Select_Challenge.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_no_preference_returns_Select_Challenge.json new file mode 100644 index 0000000000..726a77530b --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_USER_AUTH_signIn_with_no_preference_returns_Select_Challenge.json @@ -0,0 +1,60 @@ +{ + "description": "Test that USER_AUTH signIn with no preference returns Select Challenge", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "initiateAuth", + "responseType": "success", + "response": { + "challengeName": "SELECT_CHALLENGE", + "session": "someSession", + "parameters": {}, + "availableChallenges": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + ] + }, + "api": { + "name": "signIn", + "params": { + "username": "username", + "password": "" + }, + "options": { + "signInOptions": { + "authFlow": "USER_AUTH", + "preferredFirstFactor": null + } + } + }, + "validations": [ + { + "type": "amplify", + "apiName": "signIn", + "responseType": "success", + "response": { + "isSignedIn": false, + "nextStep": { + "signInStep": "CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION", + "additionalInfo": {}, + "availableFactors": [ + "PASSWORD", + "WEB_AUTHN", + "EMAIL_OTP" + ] + } + } + }, + { + "type": "state", + "expectedState": "SigningIn_SelectChallenge.json" + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json index 902a401f73..1bcaaabc32 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json @@ -55,7 +55,11 @@ "isSignUpComplete": true, "nextStep": { "signUpStep": "DONE", - "additionalInfo": { + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "", + "deliveryMedium": "UNKNOWN", + "attributeName": "" } }, "userId": "" diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_confirmed_signUp_with_valid_username_returns_CompleteAutoSignIn.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_confirmed_signUp_with_valid_username_returns_CompleteAutoSignIn.json new file mode 100644 index 0000000000..a6da373867 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_confirmed_signUp_with_valid_username_returns_CompleteAutoSignIn.json @@ -0,0 +1,70 @@ +{ + "description": "Test that passwordless confirmed signUp with valid username returns CompleteAutoSignIn", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "responseType": "success", + "response": { + "codeDeliveryDetails": { + "destination": "", + "deliveryMedium": "", + "attributeName": "" + }, + "session": "session-id", + "userConfirmed": true + } + } + ] + }, + "api": { + "name": "signUp", + "params": { + "username": "user", + "password": "" + }, + "options": { + "userAttributes": { + "email": "user@domain.com" + } + } + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "request": { + "clientId": "testAppClientId", + "username": "user", + "password": "", + "userAttributes": [ + { + "name": "email", + "value": "user@domain.com" + } + ] + } + }, + { + "type": "amplify", + "apiName": "signUp", + "responseType": "success", + "response": { + "isSignUpComplete": true, + "nextStep": { + "signUpStep": "COMPLETE_AUTO_SIGN_IN", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "", + "deliveryMedium": "UNKNOWN", + "attributeName": "" + } + }, + "userId": "" + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_existing_username_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_existing_username_fails.json new file mode 100644 index 0000000000..65085780bc --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_existing_username_fails.json @@ -0,0 +1,61 @@ +{ + "description": "Test that passwordless signUp with an existing username fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "responseType": "failure", + "response": { + "errorType": "UsernameExistsException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + ] + }, + "api": { + "name": "signUp", + "params": { + "username": "anExistingUsername", + "password": "" + }, + "options": { + "userAttributes": { + "email": "user@domain.com" + } + } + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "request": { + "clientId": "testAppClientId", + "username": "anExistingUsername", + "password": "", + "userAttributes": [ + { + "name": "email", + "value": "user@domain.com" + } + ] + } + }, + { + "type": "amplify", + "apiName": "signUp", + "responseType": "failure", + "response": { + "errorType": "UsernameExistsException", + "errorMessage": "Username already exists in the system.", + "recoverySuggestion": "Retry operation and enter another username.", + "cause": { + "errorType": "UsernameExistsException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_invalid_username_fails.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_invalid_username_fails.json new file mode 100644 index 0000000000..3797d37a7d --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_signUp_with_an_invalid_username_fails.json @@ -0,0 +1,61 @@ +{ + "description": "Test that passwordless signUp with an invalid username fails", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "responseType": "failure", + "response": { + "errorType": "InvalidParameterException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + ] + }, + "api": { + "name": "signUp", + "params": { + "username": "anInvalidUsername", + "password": "" + }, + "options": { + "userAttributes": { + "email": "user@domain.com" + } + } + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "request": { + "clientId": "testAppClientId", + "username": "anInvalidUsername", + "password": "", + "userAttributes": [ + { + "name": "email", + "value": "user@domain.com" + } + ] + } + }, + { + "type": "amplify", + "apiName": "signUp", + "responseType": "failure", + "response": { + "errorType": "InvalidParameterException", + "errorMessage": "One or more parameters are incorrect.", + "recoverySuggestion": "Enter correct parameters.", + "cause": { + "errorType": "InvalidParameterException", + "errorMessage": "Error type: Client, Protocol response: (empty response)" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_uncofirmed_signUp_with_valid_username_returns_ConfirmSignUpStep.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_uncofirmed_signUp_with_valid_username_returns_ConfirmSignUpStep.json new file mode 100644 index 0000000000..cd92169e51 --- /dev/null +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_passwordless_uncofirmed_signUp_with_valid_username_returns_ConfirmSignUpStep.json @@ -0,0 +1,69 @@ +{ + "description": "Test that passwordless uncofirmed signUp with valid username returns ConfirmSignUpStep", + "preConditions": { + "amplify-configuration": "authconfiguration_userauth.json", + "state": "SignedOut_Configured.json", + "mockedResponses": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "responseType": "success", + "response": { + "codeDeliveryDetails": { + "destination": "user@domain.com", + "deliveryMedium": "EMAIL", + "attributeName": "attributeName" + }, + "session": "session-id" + } + } + ] + }, + "api": { + "name": "signUp", + "params": { + "username": "user", + "password": "" + }, + "options": { + "userAttributes": { + "email": "user@domain.com" + } + } + }, + "validations": [ + { + "type": "cognitoIdentityProvider", + "apiName": "signUp", + "request": { + "clientId": "testAppClientId", + "username": "user", + "password": "", + "userAttributes": [ + { + "name": "email", + "value": "user@domain.com" + } + ] + } + }, + { + "type": "amplify", + "apiName": "signUp", + "responseType": "success", + "response": { + "isSignUpComplete": false, + "nextStep": { + "signUpStep": "CONFIRM_SIGN_UP_STEP", + "additionalInfo": {}, + "codeDeliveryDetails": { + "destination": "user@domain.com", + "deliveryMedium": "EMAIL", + "attributeName": "attributeName" + } + }, + "userId": "" + } + } + ] +} \ No newline at end of file diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json index 8ea70d469e..0e889e0c96 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json @@ -54,8 +54,7 @@ "isSignUpComplete": false, "nextStep": { "signUpStep": "CONFIRM_SIGN_UP_STEP", - "additionalInfo": { - }, + "additionalInfo": {}, "codeDeliveryDetails": { "destination": "user@domain.com", "deliveryMedium": "EMAIL", diff --git a/aws-core/build.gradle.kts b/aws-core/build.gradle.kts index ed80b43543..47b7a9e85e 100644 --- a/aws-core/build.gradle.kts +++ b/aws-core/build.gradle.kts @@ -38,6 +38,10 @@ dependencies { implementation(libs.aws.credentials) // slf4j dependency is added to fix https://github.com/awslabs/aws-sdk-kotlin/issues/993#issuecomment-1678885524 implementation(libs.slf4j) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotest.assertions) + testImplementation(libs.test.robolectric) } afterEvaluate { diff --git a/aws-core/src/main/java/com/amplifyframework/util/DocumentExtensions.kt b/aws-core/src/main/java/com/amplifyframework/util/DocumentExtensions.kt new file mode 100644 index 0000000000..5c74c9dc81 --- /dev/null +++ b/aws-core/src/main/java/com/amplifyframework/util/DocumentExtensions.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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.util + +import aws.smithy.kotlin.runtime.content.Document +import com.amplifyframework.annotations.InternalAmplifyApi +import java.lang.StringBuilder +import org.json.JSONArray +import org.json.JSONObject + +/** + * Converts a Smithy document to a JSON string + */ +@InternalAmplifyApi +fun Document.toJsonString(): String = buildString { appendTo(this) } + +/** + * Converts a JSON string to a Smithy document. + * NOTE: This assumes the string represents a JSON object! + */ +@Suppress("FunctionName") +@InternalAmplifyApi +fun JsonDocument(content: String): Document = DocumentBuilder().process(JSONObject(content)) + +private fun Document?.appendTo(builder: StringBuilder) { + when (val doc = this) { + is Document.String -> { + builder.append('"') + builder.append(doc.asString()) + builder.append('"') + } + is Document.Boolean -> builder.append(doc.asBoolean()) + is Document.List -> { + builder.append('[') + doc.forEachIndexed { index, document -> + document.appendTo(builder) + if (index < doc.size - 1) builder.append(',') + } + builder.append(']') + } + is Document.Map -> { + builder.append('{') + doc.entries.forEachIndexed { index, (key, value) -> + builder.append('"') + builder.append(key) + builder.append("\":") + value.appendTo(builder) + if (index < doc.size - 1) builder.append(',') + } + builder.append('}') + } + is Document.Number -> builder.append(doc.value) + null -> builder.append("null") + } +} + +internal class DocumentBuilder { + fun process(obj: JSONObject): Document.Map { + val map = mutableMapOf() + obj.keys().forEach { key -> + val document = process(obj.get(key)) + map[key] = document + } + return Document.Map(map) + } + + fun process(array: JSONArray): Document.List { + val list = mutableListOf() + for (i in 0 until array.length()) { + val document = process(array.opt(i)) + list.add(document) + } + return Document.List(list) + } + + fun process(value: Any?): Document? = when (value) { + is JSONArray -> process(value) + is JSONObject -> process(value) + is Number -> Document(value) + is String -> Document(value) + is Boolean -> Document(value) + JSONObject.NULL -> null + null -> null + else -> throw IllegalArgumentException("Unknown value type") + } +} diff --git a/aws-core/src/test/java/com/amplifyframework/util/DocumentExtensionsTest.kt b/aws-core/src/test/java/com/amplifyframework/util/DocumentExtensionsTest.kt new file mode 100644 index 0000000000..4173a7d5f8 --- /dev/null +++ b/aws-core/src/test/java/com/amplifyframework/util/DocumentExtensionsTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024 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.util + +import aws.smithy.kotlin.runtime.content.Document +import io.kotest.matchers.shouldBe +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DocumentExtensionsTest { + @Test + fun `converts number`() { + val doc = Document(42.0) + doc.toJsonString() shouldBe "42.0" + } + + @Test + fun `converts int`() { + val doc = Document(42) + doc.toJsonString() shouldBe "42" + } + + @Test + fun `converts boolean`() { + val falseDoc = Document(false) + val trueDoc = Document(true) + falseDoc.toJsonString() shouldBe "false" + trueDoc.toJsonString() shouldBe "true" + } + + @Test + fun `converts string`() { + val doc = Document("hello world") + doc.toJsonString() shouldBe "\"hello world\"" + } + + @Test + fun `converts list`() { + val list = listOf( + Document("a"), + Document(2), + null, + Document(true) + ) + val doc = Document(list) + doc.toJsonString() shouldBe """ + ["a",2,null,true] + """.trimIndent() + } + + @Test + fun `converts map`() { + val map = mapOf( + "a" to Document(1), + "b" to Document("b"), + "c" to null, + "d" to Document(true) + ) + val doc = Document(map) + doc.toJsonString() shouldBe """ + {"a":1,"b":"b","c":null,"d":true} + """.trimIndent() + } + + @Test + fun `converts complex document`() { + val doc = Document( + mapOf( + "a" to Document(1), + "b" to Document("b"), + "c" to null, + "d" to Document(listOf(Document("d1"), Document("d2"))), + "e" to Document( + mapOf( + "e1" to Document(true), + "e2" to Document(listOf(Document("e2.1"), null)), + "e3" to Document(mapOf()), + "e4" to null + ) + ) + ) + ) + + doc.toJsonString() shouldBe """ + {"a":1,"b":"b","c":null,"d":["d1","d2"],"e":{"e1":true,"e2":["e2.1",null],"e3":{},"e4":null}} + """.trimIndent() + } + + @Test + fun `converts complex json into document and back again`() { + val json = """ + {"a":1,"b":"b","c":null,"d":["d1","d2"],"e":{"e1":true,"e2":["e2.1",null],"e3":{},"e4":null}} + """.trimIndent() + + val doc = JsonDocument(json) + val result = doc.toJsonString() + + result shouldBe json + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 8365cd05ae..5c1df9c2bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,12 +43,6 @@ plugins { } allprojects { - repositories { - maven(url = "https://aws.oss.sonatype.org/content/repositories/snapshots/") - google() - mavenCentral() - } - gradle.projectsEvaluated { tasks.withType().configureEach { options.compilerArgs.apply { diff --git a/core-kotlin/api/core-kotlin.api b/core-kotlin/api/core-kotlin.api index 9d2f6b4c6f..4f3cda5317 100644 --- a/core-kotlin/api/core-kotlin.api +++ b/core-kotlin/api/core-kotlin.api @@ -55,17 +55,21 @@ public final class com/amplifyframework/kotlin/api/Rest$DefaultImpls { } public abstract interface class com/amplifyframework/kotlin/auth/Auth { + public abstract fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun autoSignIn (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun confirmSignIn (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignUpOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun deleteUser (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun fetchDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun fetchUserAttributes (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun forgetDevice (Lcom/amplifyframework/auth/AuthDevice;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getCurrentUser (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun handleWebUISignInResponse (Landroid/content/Intent;)V + public abstract fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun rememberDevice (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun resendUserAttributeConfirmationCode (Lcom/amplifyframework/auth/AuthUserAttributeKey;Lcom/amplifyframework/auth/options/AuthResendUserAttributeConfirmationCodeOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -83,11 +87,14 @@ public abstract interface class com/amplifyframework/kotlin/auth/Auth { } public final class com/amplifyframework/kotlin/auth/Auth$DefaultImpls { + public static synthetic fun associateWebAuthnCredential$default (Lcom/amplifyframework/kotlin/auth/Auth;Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun confirmResetPassword$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun confirmSignIn$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun confirmSignUp$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignUpOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun deleteWebAuthnCredential$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun fetchAuthSession$default (Lcom/amplifyframework/kotlin/auth/Auth;Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun forgetDevice$default (Lcom/amplifyframework/kotlin/auth/Auth;Lcom/amplifyframework/auth/AuthDevice;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun listWebAuthnCredentials$default (Lcom/amplifyframework/kotlin/auth/Auth;Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun resendSignUpCode$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun resendUserAttributeConfirmationCode$default (Lcom/amplifyframework/kotlin/auth/Auth;Lcom/amplifyframework/auth/AuthUserAttributeKey;Lcom/amplifyframework/auth/options/AuthResendUserAttributeConfirmationCodeOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun resetPassword$default (Lcom/amplifyframework/kotlin/auth/Auth;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResetPasswordOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; @@ -105,17 +112,21 @@ public final class com/amplifyframework/kotlin/auth/KotlinAuthFacade : com/ampli public fun ()V public fun (Lcom/amplifyframework/auth/AuthCategoryBehavior;)V public synthetic fun (Lcom/amplifyframework/auth/AuthCategoryBehavior;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun autoSignIn (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun confirmSignIn (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignUpOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun deleteUser (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun fetchDevices (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun fetchUserAttributes (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun forgetDevice (Lcom/amplifyframework/auth/AuthDevice;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getCurrentUser (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun handleWebUISignInResponse (Landroid/content/Intent;)V + public fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun rememberDevice (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resendUserAttributeConfirmationCode (Lcom/amplifyframework/auth/AuthUserAttributeKey;Lcom/amplifyframework/auth/options/AuthResendUserAttributeConfirmationCodeOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt index 8b66c28bba..e74a3da1a0 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt @@ -26,10 +26,13 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.options.AuthResetPasswordOptions @@ -40,6 +43,7 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignOutResult @@ -68,10 +72,9 @@ interface Auth { @Throws(AuthException::class) suspend fun signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions = AuthSignUpOptions.builder().build() - ): - AuthSignUpResult + ): AuthSignUpResult /** * If you have attribute confirmation enabled, this will allow the user @@ -125,8 +128,7 @@ interface Auth { username: String? = null, password: String? = null, options: AuthSignInOptions = AuthSignInOptions.defaults() - ): - AuthSignInResult + ): AuthSignInResult /** * Submit the confirmation code received as part of multi-factor Authentication during sign in. @@ -139,8 +141,14 @@ interface Auth { suspend fun confirmSignIn( challengeResponse: String, options: AuthConfirmSignInOptions = AuthConfirmSignInOptions.defaults() - ): - AuthSignInResult + ): AuthSignInResult + + /** + * Sign in the user after signed up confirmation. + * @return A sign-in result; check the nextStep field for cues on additional sign-in challenges + */ + @Throws(AuthException::class) + suspend fun autoSignIn(): AuthSignInResult /** * Launch the specified auth provider's web UI sign in experience. You should also put the @@ -157,8 +165,7 @@ interface Auth { provider: AuthProvider, callingActivity: Activity, options: AuthWebUISignInOptions = AuthWebUISignInOptions.builder().build() - ): - AuthSignInResult + ): AuthSignInResult /** * Launch a hosted web sign in UI flow. You should also put the {@link #handleWebUISignInResponse(Intent)} @@ -172,8 +179,7 @@ interface Auth { suspend fun signInWithWebUI( callingActivity: Activity, options: AuthWebUISignInOptions = AuthWebUISignInOptions.builder().build() - ): - AuthSignInResult + ): AuthSignInResult /** * Handles the response which comes back from {@link #signInWithWebUI(Activity, Consumer, Consumer)}. @@ -347,4 +353,38 @@ interface Auth { code: String, options: AuthVerifyTOTPSetupOptions = AuthVerifyTOTPSetupOptions.defaults() ) + + /** + * Create and register a passkey on this device, enabling passwordless sign in using passkeys. + * The user must be signed in to call this API. + * @param callingActivity The current Activity instance, used for launching the CredentialManager UI + * @param options Advanced options for associating credentials + */ + @Throws(AuthException::class) + suspend fun associateWebAuthnCredential( + callingActivity: Activity, + options: AuthAssociateWebAuthnCredentialsOptions = AuthAssociateWebAuthnCredentialsOptions.defaults() + ) + + /** + * Retrieve a list of WebAuthn credentials that are associated with the user's account. + * The user must be signed in to call this API. + * @param options Advanced options for listing credentials + * @return The list of associated WebAuthn credentials + */ + @Throws(AuthException::class) + suspend fun listWebAuthnCredentials( + options: AuthListWebAuthnCredentialsOptions = AuthListWebAuthnCredentialsOptions.defaults() + ): AuthListWebAuthnCredentialsResult + + /** + * Delete the credential matching the given identifier. + * @param credentialId The identifier for the credential to delete + * @param options Advanced options for deleting credentials + */ + @Throws(AuthException::class) + suspend fun deleteWebAuthnCredential( + credentialId: String, + options: AuthDeleteWebAuthnCredentialOptions = AuthDeleteWebAuthnCredentialOptions.defaults() + ) } diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt index ab9ab28267..2a2f5cb33f 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt @@ -26,10 +26,13 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.options.AuthResetPasswordOptions @@ -51,12 +54,8 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { - override suspend fun signUp( - username: String, - password: String, - options: AuthSignUpOptions - ): AuthSignUpResult { - return suspendCoroutine { continuation -> + override suspend fun signUp(username: String, password: String?, options: AuthSignUpOptions): AuthSignUpResult = + suspendCoroutine { continuation -> delegate.signUp( username, password, @@ -65,44 +64,35 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } - } override suspend fun confirmSignUp( username: String, confirmationCode: String, options: AuthConfirmSignUpOptions - ): AuthSignUpResult { - return suspendCoroutine { continuation -> - delegate.confirmSignUp( - username, - confirmationCode, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): AuthSignUpResult = suspendCoroutine { continuation -> + delegate.confirmSignUp( + username, + confirmationCode, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } override suspend fun resendSignUpCode( username: String, options: AuthResendSignUpCodeOptions - ): AuthCodeDeliveryDetails { - return suspendCoroutine { continuation -> - delegate.resendSignUpCode( - username, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): AuthCodeDeliveryDetails = suspendCoroutine { continuation -> + delegate.resendSignUpCode( + username, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun signIn( - username: String?, - password: String?, - options: AuthSignInOptions - ): AuthSignInResult { - return suspendCoroutine { continuation -> + override suspend fun signIn(username: String?, password: String?, options: AuthSignInOptions): AuthSignInResult = + suspendCoroutine { continuation -> delegate.signIn( username, password, @@ -111,13 +101,9 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } - } - override suspend fun confirmSignIn( - challengeResponse: String, - options: AuthConfirmSignInOptions - ): AuthSignInResult { - return suspendCoroutine { continuation -> + override suspend fun confirmSignIn(challengeResponse: String, options: AuthConfirmSignInOptions): AuthSignInResult = + suspendCoroutine { continuation -> delegate.confirmSignIn( challengeResponse, options, @@ -125,30 +111,30 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } + + override suspend fun autoSignIn(): AuthSignInResult = suspendCoroutine { continuation -> + delegate.autoSignIn( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } override suspend fun signInWithSocialWebUI( provider: AuthProvider, callingActivity: Activity, options: AuthWebUISignInOptions - ): - AuthSignInResult { - return suspendCoroutine { continuation -> - delegate.signInWithSocialWebUI( - provider, - callingActivity, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): AuthSignInResult = suspendCoroutine { continuation -> + delegate.signInWithSocialWebUI( + provider, + callingActivity, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun signInWithWebUI( - callingActivity: Activity, - options: AuthWebUISignInOptions - ): AuthSignInResult { - return suspendCoroutine { continuation -> + override suspend fun signInWithWebUI(callingActivity: Activity, options: AuthWebUISignInOptions): AuthSignInResult = + suspendCoroutine { continuation -> delegate.signInWithWebUI( callingActivity, options, @@ -156,62 +142,51 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } - } override fun handleWebUISignInResponse(intent: Intent) { delegate.handleWebUISignInResponse(intent) } - override suspend fun fetchAuthSession(options: AuthFetchSessionOptions): AuthSession { - return suspendCoroutine { continuation -> + override suspend fun fetchAuthSession(options: AuthFetchSessionOptions): AuthSession = + suspendCoroutine { continuation -> delegate.fetchAuthSession( options, { continuation.resume(it) }, { continuation.resumeWithException(it) } ) } + + override suspend fun rememberDevice() = suspendCoroutine { continuation -> + delegate.rememberDevice( + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun rememberDevice() { - return suspendCoroutine { continuation -> - delegate.rememberDevice( + override suspend fun forgetDevice(device: AuthDevice?) = suspendCoroutine { continuation -> + if (device == null) { + delegate.forgetDevice( { continuation.resume(Unit) }, { continuation.resumeWithException(it) } ) - } - } - - override suspend fun forgetDevice(device: AuthDevice?) { - return suspendCoroutine { continuation -> - if (device == null) { - delegate.forgetDevice( - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } else { - delegate.forgetDevice( - device, - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } - } - } - - override suspend fun fetchDevices(): List { - return suspendCoroutine { continuation -> - delegate.fetchDevices( - { continuation.resume(it) }, + } else { + delegate.forgetDevice( + device, + { continuation.resume(Unit) }, { continuation.resumeWithException(it) } ) } } - override suspend fun resetPassword( - username: String, - options: AuthResetPasswordOptions - ): AuthResetPasswordResult { - return suspendCoroutine { continuation -> + override suspend fun fetchDevices(): List = suspendCoroutine { continuation -> + delegate.fetchDevices( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + + override suspend fun resetPassword(username: String, options: AuthResetPasswordOptions): AuthResetPasswordResult = + suspendCoroutine { continuation -> delegate.resetPassword( username, options, @@ -219,93 +194,77 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } - } override suspend fun confirmResetPassword( username: String, newPassword: String, confirmationCode: String, options: AuthConfirmResetPasswordOptions - ) { - return suspendCoroutine { continuation -> - delegate.confirmResetPassword( - username, - newPassword, - confirmationCode, - options, - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } + ) = suspendCoroutine { continuation -> + delegate.confirmResetPassword( + username, + newPassword, + confirmationCode, + options, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun updatePassword(oldPassword: String, newPassword: String) { - return suspendCoroutine { continuation -> - delegate.updatePassword( - oldPassword, - newPassword, - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } + override suspend fun updatePassword(oldPassword: String, newPassword: String) = suspendCoroutine { continuation -> + delegate.updatePassword( + oldPassword, + newPassword, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun fetchUserAttributes(): List { - return suspendCoroutine { continuation -> - delegate.fetchUserAttributes( - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + override suspend fun fetchUserAttributes(): List = suspendCoroutine { continuation -> + delegate.fetchUserAttributes( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } override suspend fun updateUserAttribute( attribute: AuthUserAttribute, options: AuthUpdateUserAttributeOptions - ): AuthUpdateAttributeResult { - return suspendCoroutine { continuation -> - delegate.updateUserAttribute( - attribute, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): AuthUpdateAttributeResult = suspendCoroutine { continuation -> + delegate.updateUserAttribute( + attribute, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } override suspend fun updateUserAttributes( attributes: List, options: AuthUpdateUserAttributesOptions - ): Map { - return suspendCoroutine { continuation -> - delegate.updateUserAttributes( - attributes, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): Map = suspendCoroutine { continuation -> + delegate.updateUserAttributes( + attributes, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } override suspend fun resendUserAttributeConfirmationCode( attributeKey: AuthUserAttributeKey, options: AuthResendUserAttributeConfirmationCodeOptions - ): AuthCodeDeliveryDetails { - return suspendCoroutine { continuation -> - delegate.resendUserAttributeConfirmationCode( - attributeKey, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + ): AuthCodeDeliveryDetails = suspendCoroutine { continuation -> + delegate.resendUserAttributeConfirmationCode( + attributeKey, + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun confirmUserAttribute( - attributeKey: AuthUserAttributeKey, - confirmationCode: String - ) { - return suspendCoroutine { continuation -> + override suspend fun confirmUserAttribute(attributeKey: AuthUserAttributeKey, confirmationCode: String) = + suspendCoroutine { continuation -> delegate.confirmUserAttribute( attributeKey, confirmationCode, @@ -313,49 +272,70 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { { continuation.resumeWithException(it) } ) } - } - override suspend fun getCurrentUser(): AuthUser { - return suspendCoroutine { continuation -> - delegate.getCurrentUser( - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } + override suspend fun getCurrentUser(): AuthUser = suspendCoroutine { continuation -> + delegate.getCurrentUser( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun signOut(options: AuthSignOutOptions): AuthSignOutResult { - return suspendCoroutine { continuation -> - delegate.signOut(options) { continuation.resume(it) } - } + override suspend fun signOut(options: AuthSignOutOptions): AuthSignOutResult = suspendCoroutine { continuation -> + delegate.signOut(options) { continuation.resume(it) } } - override suspend fun deleteUser() { - return suspendCoroutine { continuation -> - delegate.deleteUser( - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } + override suspend fun deleteUser() = suspendCoroutine { continuation -> + delegate.deleteUser( + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) } - override suspend fun setUpTOTP(): TOTPSetupDetails { - return suspendCoroutine { continuation -> - delegate.setUpTOTP({ - continuation.resume(it) - }, { - continuation.resumeWithException(it) - }) - } + override suspend fun setUpTOTP(): TOTPSetupDetails = suspendCoroutine { continuation -> + delegate.setUpTOTP({ + continuation.resume(it) + }, { + continuation.resumeWithException(it) + }) } - override suspend fun verifyTOTPSetup(code: String, options: AuthVerifyTOTPSetupOptions) { - return suspendCoroutine { continuation -> + override suspend fun verifyTOTPSetup(code: String, options: AuthVerifyTOTPSetupOptions) = + suspendCoroutine { continuation -> delegate.verifyTOTPSetup(code, options, { continuation.resume(Unit) }, { continuation.resumeWithException(it) }) } + + override suspend fun associateWebAuthnCredential( + callingActivity: Activity, + options: AuthAssociateWebAuthnCredentialsOptions + ) = suspendCoroutine { continuation -> + delegate.associateWebAuthnCredential( + callingActivity, + options, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) } + + override suspend fun listWebAuthnCredentials(options: AuthListWebAuthnCredentialsOptions) = + suspendCoroutine { continuation -> + delegate.listWebAuthnCredentials( + options, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + + override suspend fun deleteWebAuthnCredential(credentialId: String, options: AuthDeleteWebAuthnCredentialOptions) = + suspendCoroutine { continuation -> + delegate.deleteWebAuthnCredential( + credentialId, + options, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) + } } diff --git a/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt b/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt index f651f409e6..1059d5ac72 100644 --- a/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt +++ b/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt @@ -32,6 +32,7 @@ import com.amplifyframework.auth.exceptions.SessionExpiredException import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.options.AuthFetchSessionOptions import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignOutResult @@ -39,10 +40,15 @@ import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.AuthUpdateAttributeResult import com.amplifyframework.core.Action import com.amplifyframework.core.Consumer +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.Answer +import io.mockk.Call import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Test @@ -1037,4 +1043,60 @@ class KotlinAuthFacadeTest { } auth.verifyTOTPSetup(code) } + + @Test + fun `associateWebAuthnCredential succeeds`() = runTest { + every { delegate.associateWebAuthnCredential(any(), any(), any(), any()) } answers CallAction(2) + auth.associateWebAuthnCredential(mockk()) + } + + @Test + fun `associateWebAuthnCredential fails`() = runTest { + val error = AuthException("oops", "bad") + every { delegate.associateWebAuthnCredential(any(), any(), any(), any()) } answers Accept(3, error) + shouldThrow { auth.associateWebAuthnCredential(mockk()) } shouldBe error + } + + @Test + fun `listWebAuthnCredentials succeeds`() = runTest { + val result = mockk() + every { delegate.listWebAuthnCredentials(any(), any(), any()) } answers Accept(1, result) + auth.listWebAuthnCredentials() shouldBe result + } + + @Test + fun `listWebAuthnCredentials fails`() = runTest { + val error = AuthException("oh", "no") + every { delegate.listWebAuthnCredentials(any(), any(), any()) } answers Accept(2, error) + shouldThrow { auth.listWebAuthnCredentials() } shouldBe error + } + + @Test + fun `deleteWebAuthnCredential succeeds`() = runTest { + val credentialId = "cred" + every { delegate.deleteWebAuthnCredential(credentialId, any(), any(), any()) } answers CallAction(2) + auth.deleteWebAuthnCredential(credentialId) + } + + @Test + fun `deleteWebAuthnCredential fails`() = runTest { + val credentialId = "cred" + val error = AuthException("oh", "no") + every { delegate.deleteWebAuthnCredential(credentialId, any(), any(), any()) } answers Accept(3, error) + shouldThrow { auth.deleteWebAuthnCredential(credentialId) } shouldBe error + } + + private class CallAction(val index: Int) : Answer { + override fun answer(call: Call) { + val action = call.invocation.args[index] as Action + action.call() + } + } + + private class Accept(val index: Int, val answer: T) : Answer { + override fun answer(call: Call) { + val onSuccess = call.invocation.args[index] as Consumer + onSuccess.accept(answer) + } + } } diff --git a/core/api/core.api b/core/api/core.api index c4e4ae8bc7..6b7539d023 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -478,6 +478,9 @@ public final class com/amplifyframework/api/rest/RestResponse$Data { public final class com/amplifyframework/auth/AuthCategory : com/amplifyframework/core/category/Category, com/amplifyframework/auth/AuthCategoryBehavior { public fun ()V + public fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun autoSignIn (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun confirmSignIn (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -486,6 +489,8 @@ public final class com/amplifyframework/auth/AuthCategory : com/amplifyframework public fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun deleteUser (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun fetchAuthSession (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun fetchDevices (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -495,6 +500,8 @@ public final class com/amplifyframework/auth/AuthCategory : com/amplifyframework public fun getCategoryType ()Lcom/amplifyframework/core/category/CategoryType; public fun getCurrentUser (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun handleWebUISignInResponse (Landroid/content/Intent;)V + public fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V + public fun listWebAuthnCredentials (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun rememberDevice (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -522,6 +529,9 @@ public final class com/amplifyframework/auth/AuthCategory : com/amplifyframework } public abstract interface class com/amplifyframework/auth/AuthCategoryBehavior { + public abstract fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public abstract fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public abstract fun autoSignIn (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun confirmSignIn (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignInOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -530,6 +540,8 @@ public abstract interface class com/amplifyframework/auth/AuthCategoryBehavior { public abstract fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun deleteUser (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public abstract fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public abstract fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun fetchAuthSession (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun fetchDevices (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -538,6 +550,8 @@ public abstract interface class com/amplifyframework/auth/AuthCategoryBehavior { public abstract fun forgetDevice (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun getCurrentUser (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun handleWebUISignInResponse (Landroid/content/Intent;)V + public abstract fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V + public abstract fun listWebAuthnCredentials (Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun rememberDevice (Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public abstract fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public abstract fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V @@ -615,6 +629,18 @@ public class com/amplifyframework/auth/AuthException : com/amplifyframework/Ampl public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } +public final class com/amplifyframework/auth/AuthFactorType : java/lang/Enum { + public static final field EMAIL_OTP Lcom/amplifyframework/auth/AuthFactorType; + public static final field PASSWORD Lcom/amplifyframework/auth/AuthFactorType; + public static final field PASSWORD_SRP Lcom/amplifyframework/auth/AuthFactorType; + public static final field SMS_OTP Lcom/amplifyframework/auth/AuthFactorType; + public static final field WEB_AUTHN Lcom/amplifyframework/auth/AuthFactorType; + public final fun getChallengeResponse ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/amplifyframework/auth/AuthFactorType; + public static fun values ()[Lcom/amplifyframework/auth/AuthFactorType; +} + public abstract class com/amplifyframework/auth/AuthPlugin : com/amplifyframework/auth/AuthCategoryBehavior, com/amplifyframework/core/plugin/Plugin { public fun ()V public final fun getCategoryType ()Lcom/amplifyframework/core/category/CategoryType; @@ -772,6 +798,22 @@ public class com/amplifyframework/auth/exceptions/ValidationException : com/ampl public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } +public abstract class com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions { + public static final field Companion Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions$Companion; + protected fun ()V + public static final fun defaults ()Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions; +} + +public abstract class com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions$Builder { + public fun ()V + public abstract fun build ()Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions; + public abstract fun getThis ()Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions$Builder; +} + +public final class com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions$Companion { + public final fun defaults ()Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions; +} + public abstract class com/amplifyframework/auth/options/AuthConfirmResetPasswordOptions { public fun ()V public static fun defaults ()Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions$DefaultAuthConfirmResetPasswordOptions; @@ -823,6 +865,22 @@ public final class com/amplifyframework/auth/options/AuthConfirmSignUpOptions$De public fun toString ()Ljava/lang/String; } +public abstract class com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions { + public static final field Companion Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions$Companion; + protected fun ()V + public static final fun defaults ()Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions; +} + +public abstract class com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions$Builder { + public fun ()V + public abstract fun build ()Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions; + public abstract fun getThis ()Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions$Builder; +} + +public final class com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions$Companion { + public final fun defaults ()Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions; +} + public class com/amplifyframework/auth/options/AuthFetchSessionOptions { public static final field Companion Lcom/amplifyframework/auth/options/AuthFetchSessionOptions$Companion; protected fun (Z)V @@ -853,6 +911,22 @@ public final class com/amplifyframework/auth/options/AuthFetchSessionOptions$Cor public fun getThis ()Lcom/amplifyframework/auth/options/AuthFetchSessionOptions$CoreBuilder; } +public abstract class com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions { + public static final field Companion Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Companion; + protected fun ()V + public static final fun defaults ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions; +} + +public abstract class com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Builder { + public fun ()V + public abstract fun build ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions; + public abstract fun getThis ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Builder; +} + +public final class com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions$Companion { + public final fun defaults ()Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions; +} + public abstract class com/amplifyframework/auth/options/AuthResendSignUpCodeOptions { public fun ()V public static fun defaults ()Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions$DefaultAuthResendSignUpCodeOptions; @@ -1042,6 +1116,10 @@ public final class com/amplifyframework/auth/options/AuthWebUISignInOptions$Core public fun getThis ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$CoreBuilder; } +public abstract interface class com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult { + public abstract fun getCredentials ()Ljava/util/List; +} + public final class com/amplifyframework/auth/result/AuthResetPasswordResult { public fun (ZLcom/amplifyframework/auth/result/step/AuthNextResetPasswordStep;)V public fun equals (Ljava/lang/Object;)Z @@ -1101,6 +1179,13 @@ public final class com/amplifyframework/auth/result/AuthUpdateAttributeResult { public fun toString ()Ljava/lang/String; } +public abstract interface class com/amplifyframework/auth/result/AuthWebAuthnCredential { + public abstract fun getCreatedAt ()Ljava/time/Instant; + public abstract fun getCredentialId ()Ljava/lang/String; + public abstract fun getFriendlyName ()Ljava/lang/String; + public abstract fun getRelyingPartyId ()Ljava/lang/String; +} + public final class com/amplifyframework/auth/result/step/AuthNextResetPasswordStep { public fun (Lcom/amplifyframework/auth/result/step/AuthResetPasswordStep;Ljava/util/Map;Lcom/amplifyframework/auth/AuthCodeDeliveryDetails;)V public fun equals (Ljava/lang/Object;)Z @@ -1112,10 +1197,11 @@ public final class com/amplifyframework/auth/result/step/AuthNextResetPasswordSt } public final class com/amplifyframework/auth/result/step/AuthNextSignInStep { - public fun (Lcom/amplifyframework/auth/result/step/AuthSignInStep;Ljava/util/Map;Lcom/amplifyframework/auth/AuthCodeDeliveryDetails;Lcom/amplifyframework/auth/TOTPSetupDetails;Ljava/util/Set;)V + public fun (Lcom/amplifyframework/auth/result/step/AuthSignInStep;Ljava/util/Map;Lcom/amplifyframework/auth/AuthCodeDeliveryDetails;Lcom/amplifyframework/auth/TOTPSetupDetails;Ljava/util/Set;Ljava/util/Set;)V public fun equals (Ljava/lang/Object;)Z public fun getAdditionalInfo ()Ljava/util/Map; public fun getAllowedMFATypes ()Ljava/util/Set; + public fun getAvailableFactors ()Ljava/util/Set; public fun getCodeDeliveryDetails ()Lcom/amplifyframework/auth/AuthCodeDeliveryDetails; public fun getSignInStep ()Lcom/amplifyframework/auth/result/step/AuthSignInStep; public fun getTotpSetupDetails ()Lcom/amplifyframework/auth/TOTPSetupDetails; @@ -1154,10 +1240,12 @@ public final class com/amplifyframework/auth/result/step/AuthSignInStep : java/l public static final field CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_NEW_PASSWORD Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_OTP Lcom/amplifyframework/auth/result/step/AuthSignInStep; + public static final field CONFIRM_SIGN_IN_WITH_PASSWORD Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_TOTP_CODE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_UP Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP Lcom/amplifyframework/auth/result/step/AuthSignInStep; + public static final field CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_MFA_SELECTION Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_TOTP_SETUP Lcom/amplifyframework/auth/result/step/AuthSignInStep; @@ -1168,6 +1256,7 @@ public final class com/amplifyframework/auth/result/step/AuthSignInStep : java/l } public final class com/amplifyframework/auth/result/step/AuthSignUpStep : java/lang/Enum { + public static final field COMPLETE_AUTO_SIGN_IN Lcom/amplifyframework/auth/result/step/AuthSignUpStep; public static final field CONFIRM_SIGN_UP_STEP Lcom/amplifyframework/auth/result/step/AuthSignUpStep; public static final field DONE Lcom/amplifyframework/auth/result/step/AuthSignUpStep; public static fun valueOf (Ljava/lang/String;)Lcom/amplifyframework/auth/result/step/AuthSignUpStep; diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java index 12bab4b2a8..edce8b8f00 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java @@ -20,10 +20,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions; import com.amplifyframework.auth.options.AuthFetchSessionOptions; +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions; import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions; import com.amplifyframework.auth.options.AuthResetPasswordOptions; @@ -34,6 +37,7 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; import com.amplifyframework.auth.result.AuthSignOutResult; @@ -63,7 +67,7 @@ public CategoryType getCategoryType() { @Override public void signUp( @NonNull String username, - @NonNull String password, + @Nullable String password, @NonNull AuthSignUpOptions options, @NonNull Consumer onSuccess, @NonNull Consumer onError @@ -412,5 +416,68 @@ public void verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTPSetupOp @NonNull Action onSuccess, @NonNull Consumer onError) { getSelectedPlugin().verifyTOTPSetup(code, options, onSuccess, onError); } + + @Override + public void associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().associateWebAuthnCredential(callingActivity, onSuccess, onError); + } + + @Override + public void associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull AuthAssociateWebAuthnCredentialsOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().associateWebAuthnCredential(callingActivity, options, onSuccess, onError); + } + + @Override + public void listWebAuthnCredentials( + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().listWebAuthnCredentials(onSuccess, onError); + } + + @Override + public void listWebAuthnCredentials( + @NonNull AuthListWebAuthnCredentialsOptions options, + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().listWebAuthnCredentials(options, onSuccess, onError); + } + + @Override + public void deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().deleteWebAuthnCredential(credentialId, onSuccess, onError); + } + + @Override + public void deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull AuthDeleteWebAuthnCredentialOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().deleteWebAuthnCredential(credentialId, options, onSuccess, onError); + } + + @Override + public void autoSignIn( + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ) { + getSelectedPlugin().autoSignIn(onSuccess, onError); + } } diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java index 6ccebd4c83..78db8d1b9f 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java @@ -20,10 +20,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions; import com.amplifyframework.auth.options.AuthFetchSessionOptions; +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions; import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions; import com.amplifyframework.auth.options.AuthResetPasswordOptions; @@ -34,6 +37,7 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; import com.amplifyframework.auth.result.AuthSignOutResult; @@ -61,7 +65,7 @@ public interface AuthCategoryBehavior { */ void signUp( @NonNull String username, - @NonNull String password, + @Nullable String password, @NonNull AuthSignUpOptions options, @NonNull Consumer onSuccess, @NonNull Consumer onError); @@ -548,4 +552,92 @@ void verifyTOTPSetup( @NonNull Action onSuccess, @NonNull Consumer onError ); + + /** + * Create and register a passkey on this device, enabling passwordless sign in using passkeys. + * The user must be signed in to call this API. + * @param callingActivity The current Activity instance, used for launching the CredentialManager UI + * @param onSuccess Success callback + * @param onError Error callback + */ + void associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); + + /** + * Create and register a passkey on this device, enabling passwordless sign in using passkeys. + * The user must be signed in to call this API. + * @param callingActivity The current Activity instance, used for launching the CredentialManager UI + * @param options Advanced options for associating credentials + * @param onSuccess Success callback + * @param onError Error callback + */ + void associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull AuthAssociateWebAuthnCredentialsOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); + + /** + * Retrieve a list of WebAuthn credentials that are associated with the user's account. + * The user must be signed in to call this API. + * @param onSuccess Success callback + * @param onError Error callback + */ + void listWebAuthnCredentials( + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ); + + /** + * Retrieve a list of WebAuthn credentials that are associated with the user's account. + * The user must be signed in to call this API. + * @param options Advanced options for listing credentials + * @param onSuccess Success callback + * @param onError Error callback + */ + void listWebAuthnCredentials( + @NonNull AuthListWebAuthnCredentialsOptions options, + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ); + + /** + * Delete the credential matching the given identifier. + * @param credentialId The identifier for the credential to delete + * @param onSuccess Success callback + * @param onError Error callback + */ + void deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); + + /** + * Delete the credential matching the given identifier. + * @param credentialId The identifier for the credential to delete + * @param options Advanced options for deleting credentials + * @param onSuccess Success callback + * @param onError Error callback + */ + void deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull AuthDeleteWebAuthnCredentialOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); + + /** + * Automatically sign in the user. + * @param onSuccess Success callback + * @param onError Error callback + */ + void autoSignIn( + @NonNull Consumer onSuccess, + @NonNull Consumer onError + ); } diff --git a/core/src/main/java/com/amplifyframework/auth/AuthFactorType.kt b/core/src/main/java/com/amplifyframework/auth/AuthFactorType.kt new file mode 100644 index 0000000000..15069e9543 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/AuthFactorType.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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 + +/** + * Enumeration of the possible mechanisms that can be used to authenticate a user when signing in with USER_AUTH. + * @param challengeResponse The response that should be sent to select a factor during the + * CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION step. + */ +enum class AuthFactorType(val challengeResponse: String) { + /** + * Sign in using the user's password + */ + PASSWORD("PASSWORD"), + + /** + * Sign in using the user's password via a Secure Remote Password flow + */ + PASSWORD_SRP("PASSWORD_SRP"), + + /** + * Sign in using a One Time Password sent to the user's email address + */ + EMAIL_OTP("EMAIL_OTP"), + + /** + * Sign in using a One Time Password sent to the user's SMS number + */ + SMS_OTP("SMS_OTP"), + + /** + * Sign in with WebAuthn (i.e. PassKey) + */ + WEB_AUTHN("WEB_AUTHN") +} diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions.kt b/core/src/main/java/com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions.kt new file mode 100644 index 0000000000..72ac7cd6be --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.options + +/** + * The shared options among all Auth plugins. + */ +abstract class AuthAssociateWebAuthnCredentialsOptions protected constructor() { + companion object { + /** + * Use the default associateWebAuthnCredential options. + * @return Default associateWebAuthnCredential options. + */ + @JvmStatic + fun defaults(): AuthAssociateWebAuthnCredentialsOptions = DefaultAuthAssociateWebAuthnCredentialOptions() + } + + /** + * The builder for this class. + * @param The type of builder - used to support plugin extensions of this. + */ + abstract class Builder> { + /** + * Return the type of builder this is so that chaining can work correctly without implicit casting. + * @return the type of builder this is + */ + abstract fun getThis(): T + + /** + * Build an instance of AuthAssociateWebAuthnCredentialsOptions (or one of its subclasses). + * @return an instance of AuthAssociateWebAuthnCredentialsOptions (or one of its subclasses) + */ + abstract fun build(): AuthAssociateWebAuthnCredentialsOptions + } + + private class DefaultAuthAssociateWebAuthnCredentialOptions : AuthAssociateWebAuthnCredentialsOptions() { + override fun hashCode() = javaClass.hashCode() + override fun toString() = javaClass.simpleName + override fun equals(other: Any?) = other is DefaultAuthAssociateWebAuthnCredentialOptions + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions.kt b/core/src/main/java/com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions.kt new file mode 100644 index 0000000000..1605fe98c0 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.options + +/** + * The shared options among all Auth plugins. + */ +abstract class AuthDeleteWebAuthnCredentialOptions protected constructor() { + companion object { + /** + * Use the default deleteWebAuthnCredential options. + * @return Default deleteWebAuthnCredential options. + */ + @JvmStatic + fun defaults(): AuthDeleteWebAuthnCredentialOptions = DefaultAuthDeleteWebAuthnCredentialOptions() + } + + /** + * The builder for this class. + * @param The type of builder - used to support plugin extensions of this. + */ + abstract class Builder> { + /** + * Return the type of builder this is so that chaining can work correctly without implicit casting. + * @return the type of builder this is + */ + abstract fun getThis(): T + + /** + * Build an instance of AuthDeleteWebAuthnCredentialOptions (or one of its subclasses). + * @return an instance of AuthDeleteWebAuthnCredentialOptions (or one of its subclasses) + */ + abstract fun build(): AuthDeleteWebAuthnCredentialOptions + } + + private class DefaultAuthDeleteWebAuthnCredentialOptions : AuthDeleteWebAuthnCredentialOptions() { + override fun hashCode() = javaClass.hashCode() + override fun toString() = javaClass.simpleName + override fun equals(other: Any?) = other is DefaultAuthDeleteWebAuthnCredentialOptions + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions.kt b/core/src/main/java/com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions.kt new file mode 100644 index 0000000000..c8d97013d9 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.options + +/** + * The shared options among all Auth plugins. + */ +abstract class AuthListWebAuthnCredentialsOptions protected constructor() { + companion object { + /** + * Use the default listWebAuthnCredentials options. + * @return Default listWebAuthnCredentials options. + */ + @JvmStatic + fun defaults(): AuthListWebAuthnCredentialsOptions = DefaultAuthListWebAuthnCredentialsOptions() + } + + /** + * The builder for this class. + * @param The type of builder - used to support plugin extensions of this. + */ + abstract class Builder> { + /** + * Return the type of builder this is so that chaining can work correctly without implicit casting. + * @return the type of builder this is + */ + abstract fun getThis(): T + /** + * Build an instance of AuthListWebAuthnCredentialsOptions (or one of its subclasses). + * @return an instance of AuthListWebAuthnCredentialsOptions (or one of its subclasses) + */ + abstract fun build(): AuthListWebAuthnCredentialsOptions + } + + private class DefaultAuthListWebAuthnCredentialsOptions : AuthListWebAuthnCredentialsOptions() { + override fun hashCode() = javaClass.hashCode() + override fun toString() = javaClass.simpleName + override fun equals(other: Any?) = other is DefaultAuthListWebAuthnCredentialsOptions + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult.kt b/core/src/main/java/com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult.kt new file mode 100644 index 0000000000..9f5eaedb0e --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/result/AuthListWebAuthnCredentialsResult.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.result + +import java.time.Instant + +/** + * A WebAuthn credential associated with the user's account + */ +interface AuthWebAuthnCredential { + /** + * The identifier for the credential + */ + val credentialId: String + + /** + * The user-readable credential name + */ + val friendlyName: String? + + /** + * The ID of the Relying Party used when registering the passkey + */ + val relyingPartyId: String + + /** + * When the credential was registered + */ + val createdAt: Instant +} + +/** + * The result returned from the listWebAuthnCredentials API + */ +interface AuthListWebAuthnCredentialsResult { + /** + * The returned credentials + */ + val credentials: List +} diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java index d404ec185c..1fb2012af7 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java @@ -20,6 +20,7 @@ import androidx.core.util.ObjectsCompat; import com.amplifyframework.auth.AuthCodeDeliveryDetails; +import com.amplifyframework.auth.AuthFactorType; import com.amplifyframework.auth.MFAType; import com.amplifyframework.auth.TOTPSetupDetails; @@ -40,6 +41,7 @@ public final class AuthNextSignInStep { private final TOTPSetupDetails totpSetupDetails; private final Set allowedMFATypes; + private final Set availableFactors; /** * Gives details on the next step, if there is one, in the sign in flow. @@ -48,19 +50,23 @@ public final class AuthNextSignInStep { * @param codeDeliveryDetails Details about how a code was sent, if relevant to the current step * @param totpSetupDetails Details to setup TOTP, if relevant to the current step * @param allowedMFATypes Set of allowed MFA type, if relevant to the current step + * @param availableFactors Set of available AuthFactorType to choose from, if relevant to the + * current step */ public AuthNextSignInStep( @NonNull AuthSignInStep signInStep, @NonNull Map additionalInfo, @Nullable AuthCodeDeliveryDetails codeDeliveryDetails, @Nullable TOTPSetupDetails totpSetupDetails, - @Nullable Set allowedMFATypes) { + @Nullable Set allowedMFATypes, + @Nullable Set availableFactors) { this.signInStep = Objects.requireNonNull(signInStep); this.additionalInfo = new HashMap<>(); this.additionalInfo.putAll(Objects.requireNonNull(additionalInfo)); this.codeDeliveryDetails = codeDeliveryDetails; this.totpSetupDetails = totpSetupDetails; this.allowedMFATypes = allowedMFATypes; + this.availableFactors = availableFactors; } /** @@ -108,6 +114,15 @@ public Set getAllowedMFATypes() { return allowedMFATypes; } + /** + * Set of available auth factors. + * @return Set of available auth factors, if relevant to the current step - null otherwise + */ + @Nullable + public Set getAvailableFactors() { + return availableFactors; + } + /** * When overriding, be sure to include signInStep, additionalInfo, and codeDeliveryDetails in the hash. * @return Hash code of this object @@ -119,7 +134,8 @@ public int hashCode() { getAdditionalInfo(), getCodeDeliveryDetails(), getTotpSetupDetails(), - getAllowedMFATypes() + getAllowedMFATypes(), + getAvailableFactors() ); } @@ -139,7 +155,8 @@ public boolean equals(Object obj) { ObjectsCompat.equals(getAdditionalInfo(), authSignUpResult.getAdditionalInfo()) && ObjectsCompat.equals(getCodeDeliveryDetails(), authSignUpResult.getCodeDeliveryDetails()) && ObjectsCompat.equals(getTotpSetupDetails(), authSignUpResult.getTotpSetupDetails()) && - ObjectsCompat.equals(getAllowedMFATypes(), authSignUpResult.getAllowedMFATypes()); + ObjectsCompat.equals(getAllowedMFATypes(), authSignUpResult.getAllowedMFATypes()) && + ObjectsCompat.equals(getAvailableFactors(), authSignUpResult.getAvailableFactors()); } } @@ -155,6 +172,7 @@ public String toString() { ", codeDeliveryDetails=" + getCodeDeliveryDetails() + ", totpSetupDetails=" + getTotpSetupDetails() + ", allowedMFATypes=" + getAllowedMFATypes() + + ", availableFactors=" + getAvailableFactors() + '}'; } } diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java index 2ad183044f..02d3d082a1 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java @@ -93,6 +93,13 @@ public enum AuthSignInStep { */ CONFIRM_SIGN_IN_WITH_TOTP_CODE, + /** + * The user is required to select which form of first factor authentication to use + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with the preferred {@link com.amplifyframework.auth.AuthFactorType}.challengeResponse. + */ + CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION, + /** * MFA is enabled on this account and requires the user to confirm with the code received by * email, sms, etc. @@ -101,6 +108,13 @@ public enum AuthSignInStep { */ CONFIRM_SIGN_IN_WITH_OTP, + /** + * The user is required to provide a password for authentication + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with the password. + */ + CONFIRM_SIGN_IN_WITH_PASSWORD, + /** * No further steps are needed in the sign in flow. */ diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignUpStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignUpStep.java index e503e69aac..96771e2b78 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignUpStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignUpStep.java @@ -28,5 +28,10 @@ public enum AuthSignUpStep { /** * The flow is completed and no further steps are needed. */ - DONE; + DONE, + + /** + * Auto sign in needs to be called to complete sign up workflow. + */ + COMPLETE_AUTO_SIGN_IN } diff --git a/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt b/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt index 5ea3013fcb..e6c847ef95 100644 --- a/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt +++ b/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt @@ -18,10 +18,13 @@ package com.amplifyframework.auth import android.app.Activity import android.content.Context import android.content.Intent +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.options.AuthResetPasswordOptions @@ -31,6 +34,7 @@ import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignOutResult @@ -62,7 +66,7 @@ class AuthPluginTest { private class TestPlugin : AuthPlugin() { override fun signUp( username: String, - password: String, + password: String?, options: AuthSignUpOptions, onSuccess: Consumer, onError: Consumer @@ -229,6 +233,41 @@ class AuthPluginTest { override fun signOut(onComplete: Consumer) {} override fun signOut(options: AuthSignOutOptions, onComplete: Consumer) {} override fun deleteUser(onSuccess: Action, onError: Consumer) {} + override fun listWebAuthnCredentials( + onSuccess: Consumer, + onError: Consumer + ) {} + override fun listWebAuthnCredentials( + options: AuthListWebAuthnCredentialsOptions, + onSuccess: Consumer, + onError: Consumer + ) { } + override fun autoSignIn(onSuccess: Consumer, onError: Consumer) {} + override fun associateWebAuthnCredential( + callingActivity: Activity, + onSuccess: Action, + onError: Consumer + ) {} + + override fun associateWebAuthnCredential( + callingActivity: Activity, + options: AuthAssociateWebAuthnCredentialsOptions, + onSuccess: Action, + onError: Consumer + ) {} + + override fun deleteWebAuthnCredential( + credentialId: String, + onSuccess: Action, + onError: Consumer + ) {} + + override fun deleteWebAuthnCredential( + credentialId: String, + options: AuthDeleteWebAuthnCredentialOptions, + onSuccess: Action, + onError: Consumer + ) {} override fun getPluginKey() = "" override fun configure(pluginConfiguration: JSONObject?, context: Context) {} override fun getEscapeHatch() = Unit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c99c98f4b3..66f81c0b1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ androidx-appcompat = "1.2.0" androidx-browser = "1.4.0" androidx-concurrent = "1.1.0" androidx-core = "1.5.0" +androidx-credentials = "1.3.0" androidx-fragment = "1.3.1" androidx-legacy = "1.0.0" androidx-lifecycle = "2.4.1" @@ -60,6 +61,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref="and androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" } androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-credentials = { module = "androidx.credentials:credentials", version.ref="androidx-credentials" } androidx-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junit-ktx" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" } androidx-nav-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigation" } diff --git a/rxbindings/api/rxbindings.api b/rxbindings/api/rxbindings.api index 172129037b..08c76f7846 100644 --- a/rxbindings/api/rxbindings.api +++ b/rxbindings/api/rxbindings.api @@ -28,6 +28,9 @@ public abstract interface class com/amplifyframework/rx/RxApiCategoryBehavior : } public abstract interface class com/amplifyframework/rx/RxAuthCategoryBehavior { + public abstract fun associateWebAuthnCredential (Landroid/app/Activity;)Lio/reactivex/rxjava3/core/Completable; + public abstract fun associateWebAuthnCredential (Landroid/app/Activity;Lcom/amplifyframework/auth/options/AuthAssociateWebAuthnCredentialsOptions;)Lio/reactivex/rxjava3/core/Completable; + public abstract fun autoSignIn ()Lio/reactivex/rxjava3/core/Single; public abstract fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/reactivex/rxjava3/core/Completable; public abstract fun confirmResetPassword (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmResetPasswordOptions;)Lio/reactivex/rxjava3/core/Completable; public abstract fun confirmSignIn (Ljava/lang/String;)Lio/reactivex/rxjava3/core/Single; @@ -36,6 +39,8 @@ public abstract interface class com/amplifyframework/rx/RxAuthCategoryBehavior { public abstract fun confirmSignUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthConfirmSignUpOptions;)Lio/reactivex/rxjava3/core/Single; public abstract fun confirmUserAttribute (Lcom/amplifyframework/auth/AuthUserAttributeKey;Ljava/lang/String;)Lio/reactivex/rxjava3/core/Completable; public abstract fun deleteUser ()Lio/reactivex/rxjava3/core/Completable; + public abstract fun deleteWebAuthnCredential (Ljava/lang/String;)Lio/reactivex/rxjava3/core/Completable; + public abstract fun deleteWebAuthnCredential (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthDeleteWebAuthnCredentialOptions;)Lio/reactivex/rxjava3/core/Completable; public abstract fun fetchAuthSession ()Lio/reactivex/rxjava3/core/Single; public abstract fun fetchAuthSession (Lcom/amplifyframework/auth/options/AuthFetchSessionOptions;)Lio/reactivex/rxjava3/core/Single; public abstract fun fetchDevices ()Lio/reactivex/rxjava3/core/Single; @@ -44,6 +49,8 @@ public abstract interface class com/amplifyframework/rx/RxAuthCategoryBehavior { public abstract fun forgetDevice (Lcom/amplifyframework/auth/AuthDevice;)Lio/reactivex/rxjava3/core/Completable; public abstract fun getCurrentUser ()Lio/reactivex/rxjava3/core/Single; public abstract fun handleWebUISignInResponse (Landroid/content/Intent;)V + public abstract fun listWebAuthnCredentials ()Lio/reactivex/rxjava3/core/Single; + public abstract fun listWebAuthnCredentials (Lcom/amplifyframework/auth/options/AuthListWebAuthnCredentialsOptions;)Lio/reactivex/rxjava3/core/Single; public abstract fun rememberDevice ()Lio/reactivex/rxjava3/core/Completable; public abstract fun resendSignUpCode (Ljava/lang/String;)Lio/reactivex/rxjava3/core/Single; public abstract fun resendSignUpCode (Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthResendSignUpCodeOptions;)Lio/reactivex/rxjava3/core/Single; diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java index 15d231db83..8b251a44c6 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java @@ -31,10 +31,13 @@ import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; import com.amplifyframework.auth.TOTPSetupDetails; +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions; import com.amplifyframework.auth.options.AuthFetchSessionOptions; +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions; import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions; import com.amplifyframework.auth.options.AuthResetPasswordOptions; @@ -45,6 +48,7 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; import com.amplifyframework.auth.result.AuthSignOutResult; @@ -117,7 +121,7 @@ public Single signIn(@Nullable String username, @Nullable Stri @Override public Single confirmSignIn( - @Nullable String challengeResponse, @NonNull AuthConfirmSignInOptions options) { + @NonNull String challengeResponse, @NonNull AuthConfirmSignInOptions options) { return toSingle((onResult, onError) -> delegate.confirmSignIn(challengeResponse, options, onResult, onError)); } @@ -127,6 +131,11 @@ public Single confirmSignIn(@NonNull String challengeResponse) return toSingle((onResult, onError) -> delegate.confirmSignIn(challengeResponse, onResult, onError)); } + @Override + public Single autoSignIn() { + return toSingle(delegate::autoSignIn); + } + @Override public Single signInWithSocialWebUI( @NonNull AuthProvider provider, @NonNull Activity callingActivity) { @@ -331,6 +340,48 @@ public Completable verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTP return toCompletable((onComplete, onError) -> delegate.verifyTOTPSetup(code, options, onComplete, onError)); } + @Override + public Completable associateWebAuthnCredential(@NonNull Activity callingActivity) { + return toCompletable((onComplete, onError) -> + delegate.associateWebAuthnCredential(callingActivity, onComplete, onError)); + } + + @Override + public Completable associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull AuthAssociateWebAuthnCredentialsOptions options + ) { + return toCompletable((onComplete, onError) -> + delegate.associateWebAuthnCredential(callingActivity, options, onComplete, onError)); + } + + @Override + public Single listWebAuthnCredentials() { + return toSingle(delegate::listWebAuthnCredentials); + } + + @Override + public Single listWebAuthnCredentials( + @NonNull AuthListWebAuthnCredentialsOptions options + ) { + return toSingle(delegate::listWebAuthnCredentials); + } + + @Override + public Completable deleteWebAuthnCredential(@NonNull String credentialId) { + return toCompletable((onComplete, onError) -> + delegate.deleteWebAuthnCredential(credentialId, onComplete, onError)); + } + + @Override + public Completable deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull AuthDeleteWebAuthnCredentialOptions options + ) { + return toCompletable(((onComplete, onError) -> + delegate.deleteWebAuthnCredential(credentialId, options, onComplete, onError))); + } + private Single toSingle(VoidBehaviors.ResultEmitter behavior) { return VoidBehaviors.toSingle(behavior); } diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java index e751f04a98..9530cf403f 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java @@ -30,10 +30,13 @@ import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; import com.amplifyframework.auth.TOTPSetupDetails; +import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; +import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions; import com.amplifyframework.auth.options.AuthFetchSessionOptions; +import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions; import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions; import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions; import com.amplifyframework.auth.options.AuthResetPasswordOptions; @@ -44,11 +47,13 @@ import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; +import com.amplifyframework.auth.result.AuthListWebAuthnCredentialsResult; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; import com.amplifyframework.auth.result.AuthSignOutResult; import com.amplifyframework.auth.result.AuthSignUpResult; import com.amplifyframework.auth.result.AuthUpdateAttributeResult; +import com.amplifyframework.auth.result.AuthWebAuthnCredential; import java.util.List; import java.util.Map; @@ -156,7 +161,7 @@ Single signIn( * {@link AuthException} on failure */ Single confirmSignIn( - @Nullable String challengeResponse, + @NonNull String challengeResponse, @NonNull AuthConfirmSignInOptions options ); @@ -168,6 +173,13 @@ Single confirmSignIn( */ Single confirmSignIn(@NonNull String challengeResponse); + /** + * Sign in the user after signed up confirmation. + * @return An Rx {@link Single} which emits {@link AuthSignInResult} on success, + * {@link AuthException} on failure + */ + Single autoSignIn(); + /** * Launch the specified auth provider's web UI sign in experience. You should also put the * {@link #handleWebUISignInResponse(Intent)} method in your activity's onNewIntent method to @@ -462,4 +474,62 @@ Single resendUserAttributeConfirmationCode( */ Completable verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTPSetupOptions options); + /** + * Create and register a passkey on this device, enabling passwordless sign in using passkeys. + * The user must be signed in to call this API. + * @param callingActivity The current Activity instance, used for launching the CredentialManager UI + * @return An Rx {@link Completable} which completes upon successfully associating a new credential; + * emits an {@link AuthException} otherwise + */ + Completable associateWebAuthnCredential(@NonNull Activity callingActivity); + + /** + * Create and register a passkey on this device, enabling passwordless sign in using passkeys. + * The user must be signed in to call this API. + * @param callingActivity The current Activity instance, used for launching the CredentialManager UI + * @param options Advanced options for associating credentials + * @return An Rx {@link Completable} which completes upon successfully associating a new credential; + * emits an {@link AuthException} otherwise + */ + Completable associateWebAuthnCredential( + @NonNull Activity callingActivity, + @NonNull AuthAssociateWebAuthnCredentialsOptions options + ); + + /** + * Retrieve a list of WebAuthn credentials that are associated with the user's account. + * The user must be signed in to call this API. + * @return An Rx {@link Single} which emits a list of {@link AuthWebAuthnCredential} on completion + */ + Single listWebAuthnCredentials(); + + /** + * Retrieve a list of WebAuthn credentials that are associated with the user's account. + * The user must be signed in to call this API. + * @param options Advanced options for listing credentials + * @return An Rx {@link Single} which emits a list of {@link AuthWebAuthnCredential} on completion + */ + Single listWebAuthnCredentials( + @NonNull AuthListWebAuthnCredentialsOptions options + ); + + /** + * Delete the credential matching the given identifier. + * @param credentialId The identifier for the credential to delete + * @return An Rx {@link Completable} which completes upon successfully deleting the credential; + * emits an {@link AuthException} otherwise + */ + Completable deleteWebAuthnCredential(@NonNull String credentialId); + + /** + * Delete the credential matching the given identifier. + * @param credentialId The identifier for the credential to delete + * @param options Advanced options for deleting credentials + * @return An Rx {@link Completable} which completes upon successfully deleting the credential; + * emits an {@link AuthException} otherwise + */ + Completable deleteWebAuthnCredential( + @NonNull String credentialId, + @NonNull AuthDeleteWebAuthnCredentialOptions options + ); } diff --git a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java index c301211fec..b9c4c1a746 100644 --- a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java +++ b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java @@ -230,7 +230,7 @@ public void testSignInSucceeds() throws InterruptedException { // Arrange a result on the result consumer AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.EMAIL); AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE; - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = username, 1 = password, 2 = onResult, 3 = onFailure @@ -292,7 +292,7 @@ public void testConfirmSignInSucceeds() throws InterruptedException { // Arrange a successful result. AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.UNKNOWN); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null, null); AuthSignInResult expected = new AuthSignInResult(true, nextStep); doAnswer(invocation -> { // 0 = confirm code, 1 = onResult, 2 = onFailure @@ -341,6 +341,63 @@ public void testConfirmSignInFails() throws InterruptedException { .assertError(failure); } + /** + * Validates that a successful call to auto sign-in will propagate the result + * back through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testAutoSignInSucceeds() throws InterruptedException { + // Arrange a successful result. + AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE; + AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.UNKNOWN); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null, null); + AuthSignInResult expected = new AuthSignInResult(true, nextStep); + doAnswer(invocation -> { + // 0 = onResult, 1 = onFailure + int positionOfResultConsumer = 0; + Consumer onResult = invocation.getArgument(positionOfResultConsumer); + onResult.accept(expected); + return null; + }).when(delegate).autoSignIn(anyConsumer(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.autoSignIn().test(); + + // Assert: result is furnished + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer + .assertNoErrors() + .assertValue(expected); + } + + /** + * Validates that a failed call to autp sign-in will propagate the failure + * back through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testAutoSignInFails() throws InterruptedException { + // Arrange a failure. + AuthException failure = new AuthException("Confirmation of sign in", " has failed.", null); + doAnswer(invocation -> { + // 0 = onResult, 1 = onFailure + int positionOfFailureConsumer = 1; + Consumer onResult = invocation.getArgument(positionOfFailureConsumer); + onResult.accept(failure); + return null; + }).when(delegate).autoSignIn(anyConsumer(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.autoSignIn().test(); + + // Assert: failure is furnished + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer + .assertNoValues() + .assertError(failure); + } + /** * Validates that a successful call to sign-in with social web UI will propagate the result * back through the binding. @@ -354,7 +411,7 @@ public void testSignInWithSocialWebUISucceeds() throws InterruptedException { // Arrange a successful result AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.PHONE); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = provider, 1 = activity, 2 = result consumer, 3 = failure consumer @@ -417,7 +474,7 @@ public void testSignInWithWebUISucceeds() throws InterruptedException { // Arrange a result AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.PHONE); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = activity, 1 = result consumer, 2 = failure consumer diff --git a/settings.gradle.kts b/settings.gradle.kts index 13326ed1b4..cda088e757 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,7 @@ pluginManagement { } dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://aws.oss.sonatype.org/content/repositories/snapshots/") diff --git a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java index 1d119db6a9..8237f1868a 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java +++ b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java @@ -129,7 +129,7 @@ public static SynchronousAuth delegatingToCognito(Context context, Plugin aut @NonNull public AuthSignUpResult signUp( @NonNull String username, - @NonNull String password, + @Nullable String password, @NonNull AuthSignUpOptions options ) throws AuthException { return Await.result(AUTH_OPERATION_TIMEOUT_MS, (onResult, onError) -> @@ -273,6 +273,16 @@ public AuthSignInResult confirmSignIn( ); } + /** + * Automatically sign in synchronously. + * @return result object + * @throws AuthException exception + */ + @NonNull + public AuthSignInResult autoSignIn() throws AuthException { + return Await.result(AUTH_OPERATION_TIMEOUT_MS, asyncDelegate::autoSignIn); + } + /** * Social web UI sign in synchronously. * @param provider The auth provider you want to launch the web ui for (e.g. Facebook, Google, etc.)