From adaf85d8a021fe5ce37a3a11ebd9e8185240b637 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:58:37 -0400 Subject: [PATCH 01/10] feat(auth): adding support for email mfa --- .../Auth/Models/AuthSignInStep.swift | 15 + Amplify/Categories/Auth/Models/MFAType.swift | 3 + .../Auth/Models/TOTPSetupDetails.swift | 2 + ...SCognitoAuthPlugin+PluginSpecificAPI.swift | 6 +- .../AWSCognitoAuthPluginBehavior.swift | 3 +- .../SignIn/InitializeResolveChallenge.swift | 50 ++- .../InitializeTOTPSetup.swift | 6 +- .../SignIn/SoftwareTokenSetup/SetUpTOTP.swift | 6 +- .../SignIn/VerifySignInChallenge.swift | 31 +- .../Models/AuthChallengeType.swift | 4 + .../Models/MFAPreference.swift | 13 + .../Models/MFATypeExtension.swift | 4 + .../CodeGen/Data/RespondToAuthChallenge.swift | 6 + .../CodeGen/Events/SetUpTOTPEvent.swift | 2 +- .../CodeGen/Events/SignInChallengeEvent.swift | 5 +- .../CodeGen/Events/SignInEvent.swift | 2 +- .../SignInChallengeState+Debug.swift | 6 +- .../CodeGen/States/SignInChallengeState.swift | 7 +- .../SignInChallengeState+Resolver.swift | 29 +- .../Helpers/UserPoolSignInHelper.swift | 60 +-- .../Task/AWSAuthConfirmSignInTask.swift | 2 +- .../Task/UpdateMFAPreferenceTask.swift | 4 + .../VerifySignInChallengeTests.swift | 21 +- .../AuthHubEventHandlerTests.swift | 2 +- .../SRPSignInState/SRPTestData.swift | 5 +- ...AuthFetchSignInSessionOperationTests.swift | 2 +- .../AWSAuthConfirmSignInTaskTests.swift | 2 +- .../SignIn/ConfirmSignInTOTPTaskTests.swift | 3 +- ...nfirmSignInWithMFASelectionTaskTests.swift | 3 +- .../SignIn/EmailMFATests.swift | 342 ++++++++++++++++++ .../SignIn/SignInSetUpTOTPTests.swift | 2 +- .../SignInChallengeState+Codable.swift | 2 +- 32 files changed, 556 insertions(+), 94 deletions(-) create mode 100644 AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift diff --git a/Amplify/Categories/Auth/Models/AuthSignInStep.swift b/Amplify/Categories/Auth/Models/AuthSignInStep.swift index e99fc9adf4..ba02152d61 100644 --- a/Amplify/Categories/Auth/Models/AuthSignInStep.swift +++ b/Amplify/Categories/Auth/Models/AuthSignInStep.swift @@ -39,6 +39,19 @@ public enum AuthSignInStep { /// case continueSignInWithMFASelection(AllowedMFATypes) + /// Auth step is for continuing sign in by setting up EMAIL multi factor authentication. + /// + case continueSignInWithEmailMFASetup + + /// Auth step is EMAIL multi factor authentication. + /// + /// Confirmation code for the MFA will be send to the provided EMAIL. + case confirmSignInWithEmailMFACode(AuthCodeDeliveryDetails) + + /// Auth step is for continuing sign in by selecting multi factor authentication type to setup + /// + case continueSignInWithMFASetupSelection(AllowedMFATypes) + /// Auth step required the user to change their password. /// case resetPassword(AdditionalInfo?) @@ -51,3 +64,5 @@ public enum AuthSignInStep { /// case done } + +extension AuthSignInStep: Equatable { } diff --git a/Amplify/Categories/Auth/Models/MFAType.swift b/Amplify/Categories/Auth/Models/MFAType.swift index 2726503aa1..4fa23c8a38 100644 --- a/Amplify/Categories/Auth/Models/MFAType.swift +++ b/Amplify/Categories/Auth/Models/MFAType.swift @@ -12,4 +12,7 @@ public enum MFAType: String { /// Time-based One Time Password linked with an authenticator app case totp + + /// Email Service linked with an email + case email } diff --git a/Amplify/Categories/Auth/Models/TOTPSetupDetails.swift b/Amplify/Categories/Auth/Models/TOTPSetupDetails.swift index 608ddcab77..7f84610180 100644 --- a/Amplify/Categories/Auth/Models/TOTPSetupDetails.swift +++ b/Amplify/Categories/Auth/Models/TOTPSetupDetails.swift @@ -40,3 +40,5 @@ public struct TOTPSetupDetails { } } + +extension TOTPSetupDetails: Equatable { } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginSpecificAPI.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginSpecificAPI.swift index 6155f93d27..bc562772da 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginSpecificAPI.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginSpecificAPI.swift @@ -41,12 +41,14 @@ public extension AWSCognitoAuthPlugin { } func updateMFAPreference( - sms: MFAPreference?, - totp: MFAPreference? + sms: MFAPreference? = nil, + totp: MFAPreference? = nil, + email: MFAPreference? = nil ) async throws { let task = UpdateMFAPreferenceTask( smsPreference: sms, totpPreference: totp, + emailPreference: email, authStateMachine: authStateMachine, userPoolFactory: authEnvironment.cognitoUserPoolFactory) return try await task.value diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPluginBehavior.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPluginBehavior.swift index ffc07ce349..feb18a7a24 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPluginBehavior.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPluginBehavior.swift @@ -49,6 +49,7 @@ protocol AWSCognitoAuthPluginBehavior: AuthCategoryPlugin { /// - totp: The preference that needs to be updated for TOTP func updateMFAPreference( sms: MFAPreference?, - totp: MFAPreference? + totp: MFAPreference?, + email: MFAPreference? ) async throws } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift index f253d2a09b..3d37525eb8 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift @@ -18,10 +18,54 @@ struct InitializeResolveChallenge: Action { func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) async { logVerbose("\(#fileID) Starting execution", environment: environment) + do { + let nextStep = try resolveNextSignInStep(for: challenge) + let event = SignInChallengeEvent(eventType: .waitForAnswer(challenge, signInMethod, nextStep)) + logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) + await dispatcher.send(event) + } catch let error as SignInError { + let errorEvent = SignInEvent(eventType: .throwAuthError(error)) + logVerbose("\(#fileID) Sending event \(errorEvent)", + environment: environment) + await dispatcher.send(errorEvent) + } catch { + let error = SignInError.service(error: error) + let errorEvent = SignInEvent(eventType: .throwAuthError(error)) + logVerbose("\(#fileID) Sending event \(errorEvent)", + environment: environment) + await dispatcher.send(errorEvent) + } + } - let event = SignInChallengeEvent(eventType: .waitForAnswer(challenge, signInMethod)) - logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) + private func resolveNextSignInStep(for challenge: RespondToAuthChallenge) throws -> AuthSignInStep { + switch challenge.challenge.authChallengeType { + case .smsMfa: + let delivery = challenge.codeDeliveryDetails + return .confirmSignInWithSMSMFACode(delivery, challenge.parameters) + case .totpMFA: + return .confirmSignInWithTOTPCode + case .customChallenge: + return .confirmSignInWithCustomChallenge(challenge.parameters) + case .newPasswordRequired: + return .confirmSignInWithNewPassword(challenge.parameters) + case .selectMFAType: + return .continueSignInWithMFASelection(challenge.getAllowedMFATypesForSelection) + case .emailMFA: + return .confirmSignInWithEmailMFACode(challenge.codeDeliveryDetails) + case .setUpMFA: + var allowedMFATypesForSetup = challenge.getAllowedMFATypesForSetup + // remove SMS, as it is not supported and should not be sent back to the customer, since it could be misleading + allowedMFATypesForSetup.remove(.sms) + if allowedMFATypesForSetup.count > 1 { + return .continueSignInWithMFASetupSelection(allowedMFATypesForSetup) + } else if let mfaType = allowedMFATypesForSetup.first, + mfaType == .email { + return .continueSignInWithEmailMFASetup + } + throw SignInError.unknown(message: "Unable to determine next step from challenge:\n\(challenge)") + case .unknown(let cognitoChallengeType): + throw SignInError.unknown(message: "Challenge not supported\(cognitoChallengeType)") + } } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/InitializeTOTPSetup.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/InitializeTOTPSetup.swift index 8fd033dae4..eca0f5fa92 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/InitializeTOTPSetup.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/InitializeTOTPSetup.swift @@ -10,7 +10,7 @@ import Foundation struct InitializeTOTPSetup: Action { var identifier: String = "InitializeTOTPSetup" - let authResponse: SignInResponseBehavior + let authResponse: RespondToAuthChallenge func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) async { logVerbose("\(#fileID) Start execution", environment: environment) @@ -26,9 +26,9 @@ extension InitializeTOTPSetup: CustomDebugDictionaryConvertible { var debugDictionary: [String: Any] { [ "identifier": identifier, - "challengeName": authResponse.challengeName?.rawValue ?? "", + "challengeName": authResponse.challenge.rawValue, "session": authResponse.session?.masked() ?? "", - "challengeParameters": authResponse.challengeParameters ?? [:] + "challengeParameters": authResponse.parameters ?? [:] ] } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/SetUpTOTP.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/SetUpTOTP.swift index ff9f8633ac..9de32b8fb1 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/SetUpTOTP.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SoftwareTokenSetup/SetUpTOTP.swift @@ -12,7 +12,7 @@ import AWSCognitoIdentityProvider struct SetUpTOTP: Action { var identifier: String = "SetUpTOTP" - let authResponse: SignInResponseBehavior + let authResponse: RespondToAuthChallenge let signInEventData: SignInEventData func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) async { @@ -65,9 +65,9 @@ extension SetUpTOTP: CustomDebugDictionaryConvertible { var debugDictionary: [String: Any] { [ "identifier": identifier, - "challengeName": authResponse.challengeName?.rawValue ?? "", + "challengeName": authResponse.challenge.rawValue, "session": authResponse.session?.masked() ?? "", - "challengeParameters": authResponse.challengeParameters ?? [:], + "challengeParameters": authResponse.parameters ?? [:], "signInEventData": signInEventData.debugDictionary ] } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift index 45c6556ce2..659be86a8c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift @@ -19,12 +19,41 @@ struct VerifySignInChallenge: Action { let signInMethod: SignInMethod + let currentSignInStep: AuthSignInStep + func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) async { logVerbose("\(#fileID) Starting execution", environment: environment) let username = challenge.username var deviceMetadata = DeviceMetadata.noData do { + + if case .continueSignInWithMFASetupSelection(_) = currentSignInStep { + let newChallenge = RespondToAuthChallenge( + challenge: .mfaSetup, + username: challenge.username, + session: challenge.session, + parameters: ["MFAS_CAN_SETUP": "[\"\(confirmSignEventData.answer)\"]"]) + + let event: SignInEvent + guard let mfaType = MFAType(rawValue: confirmSignEventData.answer) else { + throw SignInError.inputValidation(field: "Unknown MFA type") + } + + switch mfaType { + case .email: + event = SignInEvent(eventType: .receivedChallenge(newChallenge)) + case .totp: + event = SignInEvent(eventType: .initiateTOTPSetup(username, newChallenge)) + default: + throw SignInError.unknown(message: "MFA Type not supported for setup") + } + + logVerbose("\(#fileID) Sending event \(event)", environment: environment) + await dispatcher.send(event) + return + } + let userpoolEnv = try environment.userPoolEnvironment() let username = challenge.username let session = challenge.session @@ -64,7 +93,7 @@ struct VerifySignInChallenge: Action { // Remove the saved device details and retry verify challenge await DeviceMetadataHelper.removeDeviceMetaData(for: username, with: environment) let event = SignInChallengeEvent( - eventType: .retryVerifyChallengeAnswer(confirmSignEventData) + eventType: .retryVerifyChallengeAnswer(confirmSignEventData, currentSignInStep) ) logVerbose("\(#fileID) Sending event \(event)", environment: environment) await dispatcher.send(event) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AuthChallengeType.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AuthChallengeType.swift index 499aa62175..86e8d419b0 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AuthChallengeType.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AuthChallengeType.swift @@ -22,6 +22,8 @@ enum AuthChallengeType { case setUpMFA + case emailMFA + case unknown(CognitoIdentityProviderClientTypes.ChallengeNameType) } @@ -41,6 +43,8 @@ extension CognitoIdentityProviderClientTypes.ChallengeNameType { return .selectMFAType case .mfaSetup: return .setUpMFA + case .emailOtp: + return .emailMFA default: return .unknown(self) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFAPreference.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFAPreference.swift index 0b94b34d77..d2edb58d0f 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFAPreference.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFAPreference.swift @@ -52,4 +52,17 @@ extension MFAPreference { return .init(enabled: false) } } + + func emailSetting(isCurrentlyPreferred: Bool = false) -> CognitoIdentityProviderClientTypes.EmailMfaSettingsType { + switch self { + case .enabled: + return .init(enabled: true, preferredMfa: isCurrentlyPreferred) + case .preferred: + return .init(enabled: true, preferredMfa: true) + case .notPreferred: + return .init(enabled: true, preferredMfa: false) + case .disabled: + return .init(enabled: false) + } + } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFATypeExtension.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFATypeExtension.swift index afeedeb0c3..3a11701d6d 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFATypeExtension.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/MFATypeExtension.swift @@ -15,6 +15,8 @@ extension MFAType: DefaultLogger { self = .sms } else if rawValue.caseInsensitiveCompare("SOFTWARE_TOKEN_MFA") == .orderedSame { self = .totp + } else if rawValue.caseInsensitiveCompare("EMAIL_OTP") == .orderedSame { + self = .email } else { Self.log.error("Tried to initialize an unsupported MFA type with value: \(rawValue) ") return nil @@ -33,6 +35,8 @@ extension MFAType: DefaultLogger { return "SMS_MFA" case .totp: return "SOFTWARE_TOKEN_MFA" + case .email: + return "EMAIL_OTP" } } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/RespondToAuthChallenge.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/RespondToAuthChallenge.swift index c8a5297f86..70018df3a5 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/RespondToAuthChallenge.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/RespondToAuthChallenge.swift @@ -34,6 +34,8 @@ extension RespondToAuthChallenge { let destination = parameters["CODE_DELIVERY_DESTINATION"] if medium == "SMS" { deliveryDestination = .sms(destination) + } else if medium == "EMAIL" { + deliveryDestination = .email(destination) } return AuthCodeDeliveryDetails(destination: deliveryDestination, attributeKey: nil) @@ -71,6 +73,10 @@ extension RespondToAuthChallenge { case .smsMfa: return "SMS_MFA_CODE" case .softwareTokenMfa: return "SOFTWARE_TOKEN_MFA_CODE" case .newPasswordRequired: return "NEW_PASSWORD" + case .emailOtp: return "EMAIL_OTP_CODE" + // At the moment of writing this code, `mfaSetup` only supports EMAIL. + // TOTP is not part of it because, it follows a completely different setup path + case .mfaSetup: return "EMAIL" default: let message = "Unsupported challenge type for response key generation \(challenge)" let error = SignInError.unknown(message: message) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SetUpTOTPEvent.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SetUpTOTPEvent.swift index 387262e785..b100757cef 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SetUpTOTPEvent.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SetUpTOTPEvent.swift @@ -14,7 +14,7 @@ struct SetUpTOTPEvent: StateMachineEvent { enum EventType { - case setUpTOTP(SignInResponseBehavior) + case setUpTOTP(RespondToAuthChallenge) case waitForAnswer(SignInTOTPSetupData) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInChallengeEvent.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInChallengeEvent.swift index c85b03bf22..daba71bc9d 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInChallengeEvent.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInChallengeEvent.swift @@ -6,16 +6,17 @@ // import Foundation +import Amplify struct SignInChallengeEvent: StateMachineEvent { enum EventType: Equatable { - case waitForAnswer(RespondToAuthChallenge, SignInMethod) + case waitForAnswer(RespondToAuthChallenge, SignInMethod, AuthSignInStep) case verifyChallengeAnswer(ConfirmSignInEventData) - case retryVerifyChallengeAnswer(ConfirmSignInEventData) + case retryVerifyChallengeAnswer(ConfirmSignInEventData, AuthSignInStep) case verified diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInEvent.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInEvent.swift index 6733421a1f..35bce207a6 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInEvent.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInEvent.swift @@ -38,7 +38,7 @@ struct SignInEvent: StateMachineEvent { case respondDevicePasswordVerifier(SRPStateData, SignInResponseBehavior) - case initiateTOTPSetup(Username, SignInResponseBehavior) + case initiateTOTPSetup(Username, RespondToAuthChallenge) case throwPasswordVerifierError(SignInError) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/SignInChallengeState+Debug.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/SignInChallengeState+Debug.swift index 7ded00a585..4bef1e337c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/SignInChallengeState+Debug.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/SignInChallengeState+Debug.swift @@ -13,10 +13,10 @@ extension SignInChallengeState: CustomDebugDictionaryConvertible { let additionalMetadataDictionary: [String: Any] switch self { - case .waitingForAnswer(let respondAuthChallenge, _), - .verifying(let respondAuthChallenge, _, _): + case .waitingForAnswer(let respondAuthChallenge, _, _), + .verifying(let respondAuthChallenge, _, _, _): additionalMetadataDictionary = respondAuthChallenge.debugDictionary - case .error(let respondAuthChallenge, _, let error): + case .error(let respondAuthChallenge, _, let error, _): additionalMetadataDictionary = respondAuthChallenge.debugDictionary.merging( [ "error": error diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/SignInChallengeState.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/SignInChallengeState.swift index 1ad45652be..ad0651140f 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/SignInChallengeState.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/SignInChallengeState.swift @@ -6,18 +6,19 @@ // import Foundation +import Amplify enum SignInChallengeState: State { case notStarted - case waitingForAnswer(RespondToAuthChallenge, SignInMethod) + case waitingForAnswer(RespondToAuthChallenge, SignInMethod, AuthSignInStep) - case verifying(RespondToAuthChallenge, SignInMethod, String) + case verifying(RespondToAuthChallenge, SignInMethod, String, AuthSignInStep) case verified - case error(RespondToAuthChallenge, SignInMethod, SignInError) + case error(RespondToAuthChallenge, SignInMethod, SignInError, AuthSignInStep) } extension SignInChallengeState { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInChallengeState+Resolver.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInChallengeState+Resolver.swift index bff26db8a3..f14f374fc7 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInChallengeState+Resolver.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInChallengeState+Resolver.swift @@ -21,34 +21,36 @@ extension SignInChallengeState { switch oldState { case .notStarted: - if case .waitForAnswer(let challenge, let signInMethod) = event.isChallengeEvent { - return .init(newState: .waitingForAnswer(challenge, signInMethod)) + if case .waitForAnswer(let challenge, let signInMethod, let signInStep) = event.isChallengeEvent { + return .init(newState: .waitingForAnswer(challenge, signInMethod, signInStep)) } return .from(oldState) - case .waitingForAnswer(let challenge, let signInMethod): + case .waitingForAnswer(let challenge, let signInMethod, let signInStep): if case .verifyChallengeAnswer(let answerEventData) = event.isChallengeEvent { let action = VerifySignInChallenge( challenge: challenge, confirmSignEventData: answerEventData, - signInMethod: signInMethod) + signInMethod: signInMethod, + currentSignInStep: signInStep) return .init( - newState: .verifying(challenge, signInMethod, answerEventData.answer), + newState: .verifying(challenge, signInMethod, answerEventData.answer, signInStep), actions: [action] ) } return .from(oldState) - case .verifying(let challenge, let signInMethod, _): + case .verifying(let challenge, let signInMethod, _, let signInStep): - if case .retryVerifyChallengeAnswer(let answerEventData) = event.isChallengeEvent { + if case .retryVerifyChallengeAnswer(let answerEventData, let signInStep) = event.isChallengeEvent { let action = VerifySignInChallenge( challenge: challenge, confirmSignEventData: answerEventData, - signInMethod: signInMethod) + signInMethod: signInMethod, + currentSignInStep: signInStep) return .init( - newState: .verifying(challenge, signInMethod, answerEventData.answer), + newState: .verifying(challenge, signInMethod, answerEventData.answer, signInStep), actions: [action] ) } @@ -59,20 +61,21 @@ extension SignInChallengeState { } if case .throwAuthError(let error) = event.isSignInEvent { - return .init(newState: .error(challenge, signInMethod, error)) + return .init(newState: .error(challenge, signInMethod, error, signInStep)) } return .from(oldState) - case .error(let challenge, let signInMethod, _): + case .error(let challenge, let signInMethod, _, let signInStep): // If a verifyChallengeAnswer is received on error state we allow // to retry the challenge. if case .verifyChallengeAnswer(let answerEventData) = event.isChallengeEvent { let action = VerifySignInChallenge( challenge: challenge, confirmSignEventData: answerEventData, - signInMethod: signInMethod) + signInMethod: signInMethod, + currentSignInStep: signInStep) return .init( - newState: .verifying(challenge, signInMethod, answerEventData.answer), + newState: .verifying(challenge, signInMethod, answerEventData.answer, signInStep), actions: [action] ) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift index 02a2d74f62..76ce7d98a8 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift @@ -11,9 +11,9 @@ import AWSCognitoIdentityProvider struct UserPoolSignInHelper: DefaultLogger { - static func checkNextStep(_ signInState: SignInState) - throws -> AuthSignInResult? { - + static func checkNextStep( + _ signInState: SignInState + ) throws -> AuthSignInResult? { log.verbose("Checking next step for: \(signInState)") if case .signingInWithSRP(let srpState, _) = signInState, @@ -37,13 +37,13 @@ struct UserPoolSignInHelper: DefaultLogger { return try validateError(signInError: hostedUIError) } else if case .resolvingChallenge(let challengeState, _, _) = signInState, - case .error(_, _, let signInError) = challengeState { + case .error(_, _, let signInError, _) = challengeState { return try validateError(signInError: signInError) - } else if case .resolvingChallenge(let challengeState, let challengeType, _) = signInState, - case .waitingForAnswer(let challenge, _) = challengeState { - return try validateResult(for: challengeType, with: challenge) - + } else if case .resolvingChallenge(let challengeState, _, _) = signInState, + case .waitingForAnswer(_, _, let signInStep) = challengeState { + return .init(nextStep: signInStep) + } else if case .resolvingTOTPSetup(let totpSetupState, _) = signInState, case .error(_, let signInError) = totpSetupState { return try validateError(signInError: signInError) @@ -56,28 +56,6 @@ struct UserPoolSignInHelper: DefaultLogger { return nil } - private static func validateResult(for challengeType: AuthChallengeType, - with challenge: RespondToAuthChallenge) - throws -> AuthSignInResult { - switch challengeType { - case .smsMfa: - let delivery = challenge.codeDeliveryDetails - return .init(nextStep: .confirmSignInWithSMSMFACode(delivery, challenge.parameters)) - case .totpMFA: - return .init(nextStep: .confirmSignInWithTOTPCode) - case .customChallenge: - return .init(nextStep: .confirmSignInWithCustomChallenge(challenge.parameters)) - case .newPasswordRequired: - return .init(nextStep: .confirmSignInWithNewPassword(challenge.parameters)) - case .selectMFAType: - return .init(nextStep: .continueSignInWithMFASelection(challenge.getAllowedMFATypesForSelection)) - case .setUpMFA: - throw AuthError.unknown("Invalid state flow. setUpMFA is handled internally in `SignInState.resolvingTOTPSetup` state.") - case .unknown(let cognitoChallengeType): - throw AuthError.unknown("Challenge not supported\(cognitoChallengeType)", nil) - } - } - private static func validateError(signInError: SignInError) throws -> AuthSignInResult { if signInError.isUserNotConfirmed { return AuthSignInResult(nextStep: .confirmSignUp(nil)) @@ -136,19 +114,27 @@ struct UserPoolSignInHelper: DefaultLogger { parameters: parameters) switch challengeName { - case .smsMfa, .customChallenge, .newPasswordRequired, .softwareTokenMfa, .selectMfaType: + case .smsMfa, .customChallenge, .newPasswordRequired, .softwareTokenMfa, .selectMfaType, .emailOtp: return SignInEvent(eventType: .receivedChallenge(respondToAuthChallenge)) case .deviceSrpAuth: return SignInEvent(eventType: .initiateDeviceSRP(username, response)) case .mfaSetup: let allowedMFATypesForSetup = respondToAuthChallenge.getAllowedMFATypesForSetup - if allowedMFATypesForSetup.contains(.totp) { - return SignInEvent(eventType: .initiateTOTPSetup(username, response)) + if allowedMFATypesForSetup.contains(.totp) && allowedMFATypesForSetup.contains(.email) { + return SignInEvent(eventType: .receivedChallenge(respondToAuthChallenge)) + } else if allowedMFATypesForSetup.contains(.totp) { + return SignInEvent(eventType: .initiateTOTPSetup(username, respondToAuthChallenge)) + } else if allowedMFATypesForSetup.contains(.email) { + return SignInEvent(eventType: .receivedChallenge(respondToAuthChallenge)) } else { let message = "Cannot initiate MFA setup from available Types: \(allowedMFATypesForSetup)" let error = SignInError.invalidServiceResponse(message: message) return SignInEvent(eventType: .throwAuthError(error)) } + case.sdkUnknown(let challengeType): + let message = "Unsupported challenge response \(challengeName)" + let error = SignInError.unknown(message: message) + return SignInEvent(eventType: .throwAuthError(error)) default: let message = "Unsupported challenge response \(challengeName)" let error = SignInError.unknown(message: message) @@ -160,12 +146,4 @@ struct UserPoolSignInHelper: DefaultLogger { return SignInEvent(eventType: .throwAuthError(error)) } } - - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift index cc824c6f25..9b6ed906e3 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift @@ -55,7 +55,7 @@ class AWSAuthConfirmSignInTask: AuthConfirmSignInTask, DefaultLogger { if case .resolvingChallenge(let challengeState, let challengeType, _) = signInState { // Validate if request valid MFA selection - if case .selectMFAType = challengeType { + if challengeType == .selectMFAType { try validateRequestForMFASelection() } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UpdateMFAPreferenceTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UpdateMFAPreferenceTask.swift index b9bedf4fe8..3bfdadb427 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UpdateMFAPreferenceTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UpdateMFAPreferenceTask.swift @@ -26,6 +26,7 @@ class UpdateMFAPreferenceTask: AuthUpdateMFAPreferenceTask, DefaultLogger { private let smsPreference: MFAPreference? private let totpPreference: MFAPreference? + private let emailPreference: MFAPreference? private let authStateMachine: AuthStateMachine private let userPoolFactory: CognitoUserPoolFactory private let taskHelper: AWSAuthTaskHelper @@ -36,10 +37,12 @@ class UpdateMFAPreferenceTask: AuthUpdateMFAPreferenceTask, DefaultLogger { init(smsPreference: MFAPreference?, totpPreference: MFAPreference?, + emailPreference: MFAPreference?, authStateMachine: AuthStateMachine, userPoolFactory: @escaping CognitoUserPoolFactory) { self.smsPreference = smsPreference self.totpPreference = totpPreference + self.emailPreference = emailPreference self.authStateMachine = authStateMachine self.userPoolFactory = userPoolFactory self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) @@ -63,6 +66,7 @@ class UpdateMFAPreferenceTask: AuthUpdateMFAPreferenceTask, DefaultLogger { let preferredMFAType = currentPreference.preferredMfaSetting.map(MFAType.init(rawValue:)) let input = SetUserMFAPreferenceInput( accessToken: accessToken, + emailMfaSettings: emailPreference?.emailSetting(isCurrentlyPreferred: preferredMFAType == .email), smsMfaSettings: smsPreference?.smsSetting(isCurrentlyPreferred: preferredMFAType == .sms), softwareTokenMfaSettings: totpPreference?.softwareTokenSetting(isCurrentlyPreferred: preferredMFAType == .totp)) _ = try await userPoolService.setUserMFAPreference(input: input) diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/VerifySignInChallenge/VerifySignInChallengeTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/VerifySignInChallenge/VerifySignInChallengeTests.swift index 9937d86b74..c67a2bc08e 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/VerifySignInChallenge/VerifySignInChallengeTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/VerifySignInChallenge/VerifySignInChallengeTests.swift @@ -49,7 +49,8 @@ class VerifySignInChallengeTests: XCTestCase { userPoolFactory: identityProviderFactory) let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) await action.execute( withDispatcher: MockDispatcher { _ in }, @@ -84,7 +85,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let passwordVerifierError = expectation(description: "passwordVerifierError") @@ -133,7 +135,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let verifyChallengeComplete = expectation(description: "verifyChallengeComplete") @@ -183,7 +186,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let passwordVerifierError = expectation( description: "passwordVerifierError") @@ -233,7 +237,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let passwordVerifierError = expectation(description: "passwordVerifierError") let dispatcher = MockDispatcher { event in @@ -279,7 +284,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let verifyChallengeComplete = expectation(description: "verifyChallengeComplete") @@ -323,7 +329,8 @@ class VerifySignInChallengeTests: XCTestCase { let action = VerifySignInChallenge(challenge: mockRespondAuthChallenge, confirmSignEventData: mockConfirmEvent, - signInMethod: .apiBased(.userSRP)) + signInMethod: .apiBased(.userSRP), + currentSignInStep: .confirmSignInWithTOTPCode) let verifyChallengeComplete = expectation(description: "verifyChallengeComplete") diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HubEventTests/AuthHubEventHandlerTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HubEventTests/AuthHubEventHandlerTests.swift index 496e7d2331..d59ec0a9cb 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HubEventTests/AuthHubEventHandlerTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HubEventTests/AuthHubEventHandlerTests.swift @@ -335,7 +335,7 @@ class AuthHubEventHandlerTests: XCTestCase { private func configurePluginForConfirmSignInEvent() { let initialState = AuthState.configured( AuthenticationState.signingIn(.resolvingChallenge( - .waitingForAnswer(.testData(), .apiBased(.userSRP)), + .waitingForAnswer(.testData(), .apiBased(.userSRP), .confirmSignInWithTOTPCode), .smsMfa, .apiBased(.userSRP))), AuthorizationState.sessionEstablished(.testData)) diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SRPSignInState/SRPTestData.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SRPSignInState/SRPTestData.swift index 4ecd257a5d..c34a53b83d 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SRPSignInState/SRPTestData.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SRPSignInState/SRPTestData.swift @@ -110,12 +110,13 @@ extension RespondToAuthChallengeOutput { static func testData( challenge: CognitoIdentityProviderClientTypes.ChallengeNameType = .smsMfa, - challengeParameters: [String: String] = [:]) -> RespondToAuthChallengeOutput { + challengeParameters: [String: String] = [:], + session: String = "session") -> RespondToAuthChallengeOutput { return RespondToAuthChallengeOutput( authenticationResult: nil, challengeName: challenge, challengeParameters: challengeParameters, - session: "session") + session: session) } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift index a67aa2522b..6016c04bfa 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift @@ -755,7 +755,7 @@ class AWSAuthFetchSignInSessionOperationTests: BaseAuthorizationTests { /// func testSessionWhenWaitingConfirmSignIn() async throws { let signInMethod = SignInMethod.apiBased(.userSRP) - let challenge = SignInChallengeState.waitingForAnswer(.testData(), signInMethod) + let challenge = SignInChallengeState.waitingForAnswer(.testData(), signInMethod, .confirmSignInWithTOTPCode) let initialState = AuthState.configured( AuthenticationState.signingIn( .resolvingChallenge(challenge, .smsMfa, signInMethod)), diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInTaskTests.swift index da1a27739a..7e1a3ee6d1 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInTaskTests.swift @@ -19,7 +19,7 @@ class AuthenticationProviderConfirmSigninTests: BasePluginTest { override var initialState: AuthState { AuthState.configured( AuthenticationState.signingIn( - .resolvingChallenge(.waitingForAnswer(.testData(), .apiBased(.userSRP)), + .resolvingChallenge(.waitingForAnswer(.testData(), .apiBased(.userSRP), .confirmSignInWithTOTPCode), .smsMfa, .apiBased(.userSRP))), AuthorizationState.sessionEstablished(.testData)) } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInTOTPTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInTOTPTaskTests.swift index 98849dce07..473d673a4a 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInTOTPTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInTOTPTaskTests.swift @@ -22,7 +22,8 @@ class ConfirmSignInTOTPTaskTests: BasePluginTest { .resolvingChallenge( .waitingForAnswer( .testData(challenge: .softwareTokenMfa), - .apiBased(.userSRP) + .apiBased(.userSRP), + .confirmSignInWithTOTPCode ), .totpMFA, .apiBased(.userSRP))), diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInWithMFASelectionTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInWithMFASelectionTaskTests.swift index da3e22fa6c..edb5b8687e 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInWithMFASelectionTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/ConfirmSignInWithMFASelectionTaskTests.swift @@ -22,7 +22,8 @@ class ConfirmSignInWithMFASelectionTaskTests: BasePluginTest { .resolvingChallenge( .waitingForAnswer( .testData(challenge: .selectMfaType), - .apiBased(.userSRP) + .apiBased(.userSRP), + .confirmSignInWithTOTPCode ), .selectMFAType, .apiBased(.userSRP))), diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift new file mode 100644 index 0000000000..7be4e8960e --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift @@ -0,0 +1,342 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import AWSCognitoIdentity +@testable import Amplify +@testable import AWSCognitoAuthPlugin +import AWSCognitoIdentityProvider +import AWSClientRuntime + +class EmailMFATests: BasePluginTest { + + override var initialState: AuthState { + AuthState.configured(.signedOut(.init(lastKnownUserName: nil)), .configured) + } + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testSuccessfulMFASetupSelectionStep() async { + self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { input in + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .passwordVerifier, + challengeParameters: InitiateAuthOutput.validChalengeParams, + session: "someSession") + }, mockRespondToAuthChallengeResponse: { input in + return .testData( + challenge: .mfaSetup, + challengeParameters: ["MFAS_CAN_SETUP": "[\"SMS_MFA\",\"SOFTWARE_TOKEN_MFA\",\"EMAIL_OTP\"]"]) + }) + let options = AuthSignInRequest.Options() + + do { + let result = try await plugin.signIn( + username: "username", + password: "password", + options: options) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } + + /// Test a signIn with valid inputs getting continueSignInWithEmailMFASetup challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithEmailMFASetup response + /// + func testSuccessfulEmailMFASetupStep() async { + self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { input in + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .passwordVerifier, + challengeParameters: InitiateAuthOutput.validChalengeParams, + session: "someSession") + }, mockRespondToAuthChallengeResponse: { input in + return .testData( + challenge: .mfaSetup, + challengeParameters: ["MFAS_CAN_SETUP": "[\"SMS_MFA\",\"EMAIL_OTP\"]"]) + }) + let options = AuthSignInRequest.Options() + + do { + let result = try await plugin.signIn( + username: "username", + password: "password", + options: options) + guard case .continueSignInWithEmailMFASetup = result.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup for next step, instead got: \(result.nextStep)") + return + } + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } + + /// Test a signIn with valid inputs getting confirmSignInWithEmailMFACode challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .confirmSignInWithEmailMFACode response + /// + func testSuccessfulEmailMFACodeStep() async { + var signInStepIterator = 0 + self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { input in + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .passwordVerifier, + challengeParameters: InitiateAuthOutput.validChalengeParams, + session: "someSession") + }, mockRespondToAuthChallengeResponse: { input in + if signInStepIterator == 0 { + return .testData( + challenge: .emailOtp, + challengeParameters: [ + "CODE_DELIVERY_DELIVERY_MEDIUM": "EMAIL", + "CODE_DELIVERY_DESTINATION": "test@test.com"]) + } else if signInStepIterator == 1 { + XCTAssertEqual(input.challengeResponses?["EMAIL_OTP_CODE"], "123456") + XCTAssertEqual(input.session, "session") + return .testData() + } + fatalError("not supported code path") + }) + + do { + let result = try await plugin.signIn( + username: "username", + password: "password", + options: AuthSignInRequest.Options()) + guard case .confirmSignInWithEmailMFACode(let codeDetails) = result.nextStep else { + XCTFail("Result should be .confirmSignInWithEmailMFACode for next step, instead got: \(result.nextStep)") + return + } + if case .email(let destination) = codeDetails.destination { + XCTAssertEqual(destination, "test@test.com") + } else { + XCTFail("Destination should be email") + } + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // step 2: confirm sign in + signInStepIterator = 1 + let confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: "123456", + options: .init()) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } + + + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testConfirmSignInForEmailMFASetupSelectionStep() async { + var signInStepIterator = 0 + self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { input in + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .passwordVerifier, + challengeParameters: InitiateAuthOutput.validChalengeParams, + session: "session0") + }, mockRespondToAuthChallengeResponse: { input in + switch signInStepIterator { + case 0: + XCTAssertEqual(input.session, "session0") + return .testData( + challenge: .mfaSetup, + challengeParameters: ["MFAS_CAN_SETUP": "[\"SMS_MFA\",\"SOFTWARE_TOKEN_MFA\",\"EMAIL_OTP\"]"], + session: "session1") + case 1: + XCTAssertEqual(input.challengeResponses?["EMAIL"], "test@test.com") + XCTAssertEqual(input.session, "session1") + return .testData( + challenge: .emailOtp, + challengeParameters: [ + "CODE_DELIVERY_DELIVERY_MEDIUM": "EMAIL", + "CODE_DELIVERY_DESTINATION": "test@test.com"], + session: "session2") + case 2: + XCTAssertEqual(input.challengeResponses?["EMAIL_OTP_CODE"], "123456") + XCTAssertEqual(input.session, "session2") + return .testData() + default: fatalError("unsupported path") + } + + }) + + do { + // Step 1: initiate sign in + let result = try await plugin.signIn( + username: "username", + password: "password", + options: AuthSignInRequest.Options()) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 2: select email to continue setting up + var confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: MFAType.email.challengeResponse) + guard case .continueSignInWithEmailMFASetup = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + + // Step 3: pass an email to setup + signInStepIterator = 1 + confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: "test@test.com") + guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + if case .email(let destination) = deliveryDetails.destination { + XCTAssertEqual(destination, "test@test.com") + } else { + XCTFail("Destination should be email") + } + + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // step 4: confirm sign in + signInStepIterator = 2 + confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: "123456", + options: .init()) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + + } catch { + XCTFail("Received failure with error \(error)") + } + } + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testConfirmSignInForTOTPMFASetupSelectionStep() async { + var completeSignIn = false + self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { input in + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .passwordVerifier, + challengeParameters: InitiateAuthOutput.validChalengeParams, + session: "someSession") + }, mockRespondToAuthChallengeResponse: { input in + if completeSignIn { + XCTAssertEqual(input.session, "verifiedSession") + return .testData() + } + + return .testData( + challenge: .mfaSetup, + challengeParameters: ["MFAS_CAN_SETUP": "[\"SMS_MFA\",\"SOFTWARE_TOKEN_MFA\",\"EMAIL_OTP\"]"]) + + + }, mockAssociateSoftwareTokenResponse: { input in + return .init(secretCode: "sharedSecret", session: "newSession") + }, mockVerifySoftwareTokenResponse: { request in + XCTAssertEqual(request.session, "newSession") + XCTAssertEqual(request.userCode, "123456") + XCTAssertEqual(request.friendlyDeviceName, "device") + return .init(session: "verifiedSession", status: .success) + }) + + do { + // Step 1: initiate sign in + let result = try await plugin.signIn( + username: "username", + password: "password", + options: AuthSignInRequest.Options()) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 2: continue sign in by selecting TOTP for set up + var confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: MFAType.totp.challengeResponse) + guard case .continueSignInWithTOTPSetup(let totpDetails) = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + XCTAssertEqual(totpDetails.sharedSecret, "sharedSecret") + XCTAssertEqual(totpDetails.username, "royji2") + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 3: complete sign in by verifying TOTP set up + completeSignIn = true + let pluginOptions = AWSAuthConfirmSignInOptions(friendlyDeviceName: "device") + confirmSignInResult = try await plugin.confirmSignIn( + challengeResponse: "123456", + options: .init(pluginOptions: pluginOptions)) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + + } catch { + XCTFail("Received failure with error \(error)") + } + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/SignInSetUpTOTPTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/SignInSetUpTOTPTests.swift index 387a70f340..6c3d63f90f 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/SignInSetUpTOTPTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/SignInSetUpTOTPTests.swift @@ -76,7 +76,7 @@ class SignInSetUpTOTPTests: BasePluginTest { session: "session") }, mockAssociateSoftwareTokenResponse: { _ in return .init(secretCode: "123456", session: "session") - } ) + }) let options = AuthSignInRequest.Options() do { diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TestHarness/CodableStates/SignInChallengeState+Codable.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TestHarness/CodableStates/SignInChallengeState+Codable.swift index 467535a3c9..f2d200309f 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TestHarness/CodableStates/SignInChallengeState+Codable.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TestHarness/CodableStates/SignInChallengeState+Codable.swift @@ -31,7 +31,7 @@ extension SignInChallengeState: Codable { username: try nestedContainerValue.decode(String.self, forKey: .username), session: try nestedContainerValue.decode(String.self, forKey: .session), parameters: try nestedContainerValue.decode([String: String].self, forKey: .parameters)), - .apiBased(.userSRP)) + .apiBased(.userSRP), .confirmSignInWithTOTPCode) } else { fatalError("Decoding not supported") } From f48b356130d7e9627fe02e907a4a783961371de2 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:03:17 -0400 Subject: [PATCH 02/10] fix swift lint warning --- .../Actions/SignIn/VerifySignInChallenge.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift index 659be86a8c..0beead2f68 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift @@ -28,7 +28,7 @@ struct VerifySignInChallenge: Action { do { - if case .continueSignInWithMFASetupSelection(_) = currentSignInStep { + if case .continueSignInWithMFASetupSelection = currentSignInStep { let newChallenge = RespondToAuthChallenge( challenge: .mfaSetup, username: challenge.username, From ba3f71bd58e356f7d6b3e1350d40b2cecd0086b0 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:05:47 -0400 Subject: [PATCH 03/10] worked on a review comment --- .../Support/Helpers/UserPoolSignInHelper.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift index 76ce7d98a8..05097b818c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift @@ -131,10 +131,6 @@ struct UserPoolSignInHelper: DefaultLogger { let error = SignInError.invalidServiceResponse(message: message) return SignInEvent(eventType: .throwAuthError(error)) } - case.sdkUnknown(let challengeType): - let message = "Unsupported challenge response \(challengeName)" - let error = SignInError.unknown(message: message) - return SignInEvent(eventType: .throwAuthError(error)) default: let message = "Unsupported challenge response \(challengeName)" let error = SignInError.unknown(message: message) From 18a8c701faadb72b2a26fec8672d842d3e2b9cd4 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:58:47 -0400 Subject: [PATCH 04/10] adding integration tests wave 1 --- .../AuthHostApp.xcodeproj/project.pbxproj | 31 ++ .../AWSAuthBaseTest.swift | 83 +++++- .../Helpers/AuthSignInHelper.swift | 20 +- .../EmailMFATests/EmailMFAOnlyTests.swift | 69 +++++ ...EmailMFAWithAllMFATypesRequiredTests.swift | 272 ++++++++++++++++++ 5 files changed, 469 insertions(+), 6 deletions(-) create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj index 2444c39e1a..14a1cccaff 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj @@ -61,6 +61,13 @@ 485CB5C027B61F1E006CCEC7 /* SignedOutAuthSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BC27B61F1D006CCEC7 /* SignedOutAuthSessionTests.swift */; }; 485CB5C127B61F1E006CCEC7 /* AuthSignOutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BD27B61F1D006CCEC7 /* AuthSignOutTests.swift */; }; 485CB5C227B61F1E006CCEC7 /* AuthSRPSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BE27B61F1D006CCEC7 /* AuthSRPSignInTests.swift */; }; + 487C40232CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C40222CACF2FD009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift */; }; + 487C40242CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C40222CACF2FD009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift */; }; + 487C40252CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C40222CACF2FD009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift */; }; + 487C40382CACFD50009CF221 /* AWSAPIPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 487C40372CACFD50009CF221 /* AWSAPIPlugin */; }; + 487C403F2CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; + 487C40402CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; + 487C40412CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; 48916F382A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F372A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift */; }; 48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F392A412CEE00E3E1B1 /* TOTPHelper.swift */; }; 48916F3C2A42333E00E3E1B1 /* MFAPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F3B2A42333E00E3E1B1 /* MFAPreferenceTests.swift */; }; @@ -196,6 +203,8 @@ 485CB5BC27B61F1D006CCEC7 /* SignedOutAuthSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignedOutAuthSessionTests.swift; sourceTree = ""; }; 485CB5BD27B61F1D006CCEC7 /* AuthSignOutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthSignOutTests.swift; sourceTree = ""; }; 485CB5BE27B61F1D006CCEC7 /* AuthSRPSignInTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthSRPSignInTests.swift; sourceTree = ""; }; + 487C40222CACF2FD009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailMFAWithAllMFATypesRequiredTests.swift; sourceTree = ""; }; + 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailMFAOnlyTests.swift; sourceTree = ""; }; 48916F372A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPSetupWhenAuthenticatedTests.swift; sourceTree = ""; }; 48916F392A412CEE00E3E1B1 /* TOTPHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPHelper.swift; sourceTree = ""; }; 48916F3B2A42333E00E3E1B1 /* MFAPreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFAPreferenceTests.swift; sourceTree = ""; }; @@ -232,6 +241,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 487C40382CACFD50009CF221 /* AWSAPIPlugin in Frameworks */, B4B9F45828F47C0A004F346F /* Amplify in Frameworks */, B4B9F45A28F47C0A004F346F /* AWSCognitoAuthPlugin in Frameworks */, ); @@ -432,9 +442,19 @@ name = Packages; sourceTree = ""; }; + 487C403D2CADBC37009CF221 /* EmailMFATests */ = { + isa = PBXGroup; + children = ( + 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */, + 487C40222CACF2FD009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift */, + ); + path = EmailMFATests; + sourceTree = ""; + }; 48916F362A412AF800E3E1B1 /* MFATests */ = { isa = PBXGroup; children = ( + 487C403D2CADBC37009CF221 /* EmailMFATests */, 48916F372A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift */, 48916F3B2A42333E00E3E1B1 /* MFAPreferenceTests.swift */, 48599D492A429893009DE21C /* MFASignInTests.swift */, @@ -534,6 +554,7 @@ packageProductDependencies = ( B4B9F45728F47C0A004F346F /* Amplify */, B4B9F45928F47C0A004F346F /* AWSCognitoAuthPlugin */, + 487C40372CACFD50009CF221 /* AWSAPIPlugin */, ); productName = AuthHostApp; productReference = 485CB53A27B614CE006CCEC7 /* AuthHostApp.app */; @@ -819,6 +840,7 @@ 21F762B22BD6B1AA0048845A /* SignedOutAuthSessionTests.swift in Sources */, 21F762B32BD6B1AA0048845A /* AuthSignInHelper.swift in Sources */, 21F762B42BD6B1AA0048845A /* FederatedSessionTests.swift in Sources */, + 487C40402CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */, 21F762B52BD6B1AA0048845A /* AuthCustomSignInTests.swift in Sources */, 21F762B62BD6B1AA0048845A /* AuthEventIntegrationTests.swift in Sources */, 21F762B72BD6B1AA0048845A /* AuthEnvironmentHelper.swift in Sources */, @@ -832,6 +854,7 @@ 21F762BF2BD6B1AA0048845A /* MFASignInTests.swift in Sources */, 21F762C02BD6B1AA0048845A /* SignedInAuthSessionTests.swift in Sources */, 21F762C12BD6B1AA0048845A /* AuthSignUpTests.swift in Sources */, + 487C40252CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */, 21F762C22BD6B1AA0048845A /* AuthConfirmResetPasswordTests.swift in Sources */, 21F762C32BD6B1AA0048845A /* AuthDeleteUserTests.swift in Sources */, ); @@ -851,6 +874,7 @@ buildActionMask = 2147483647; files = ( 485CB5B927B61F10006CCEC7 /* AuthSessionHelper.swift in Sources */, + 487C403F2CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */, 681DFEAB28E747B80000C36A /* AsyncTesting.swift in Sources */, 485CB5C227B61F1E006CCEC7 /* AuthSRPSignInTests.swift in Sources */, 9737C7502880BFD600DA0D2B /* AuthForgetDeviceTests.swift in Sources */, @@ -863,6 +887,7 @@ 48E3AB3128E52590004EE395 /* GetCurrentUserTests.swift in Sources */, 48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */, 21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */, + 487C40232CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */, 485CB5B127B61EAC006CCEC7 /* AWSAuthBaseTest.swift in Sources */, 485CB5C027B61F1E006CCEC7 /* SignedOutAuthSessionTests.swift in Sources */, 485CB5BA27B61F10006CCEC7 /* AuthSignInHelper.swift in Sources */, @@ -914,6 +939,7 @@ 681B76AC2A3CBBAE004B59D9 /* AWSAuthBaseTest.swift in Sources */, 681B76AD2A3CBBAE004B59D9 /* SignedOutAuthSessionTests.swift in Sources */, 681B76AE2A3CBBAE004B59D9 /* AuthSignInHelper.swift in Sources */, + 487C40412CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */, 681B76AF2A3CBBAE004B59D9 /* FederatedSessionTests.swift in Sources */, 681B76B02A3CBBAE004B59D9 /* AuthCustomSignInTests.swift in Sources */, 681B76B12A3CBBAE004B59D9 /* AuthEventIntegrationTests.swift in Sources */, @@ -927,6 +953,7 @@ 681B76B92A3CBBAE004B59D9 /* SignedInAuthSessionTests.swift in Sources */, 681B76BA2A3CBBAE004B59D9 /* AuthSignUpTests.swift in Sources */, 681B76BB2A3CBBAE004B59D9 /* AuthConfirmResetPasswordTests.swift in Sources */, + 487C40242CACF303009CF221 /* EmailMFAWithAllMFATypesRequiredTests.swift in Sources */, 48BCE8942A54564C0012C3CD /* MFASignInTests.swift in Sources */, 681B76BC2A3CBBAE004B59D9 /* AuthDeleteUserTests.swift in Sources */, ); @@ -1479,6 +1506,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 487C40372CACFD50009CF221 /* AWSAPIPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSAPIPlugin; + }; 681B76992A3CBA97004B59D9 /* Amplify */ = { isa = XCSwiftPackageProductDependency; productName = Amplify; diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift index 69668eee0d..7bbef1de02 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift @@ -34,8 +34,10 @@ class AWSAuthBaseTest: XCTestCase { var amplifyConfiguration: AmplifyConfiguration! var amplifyOutputs: AmplifyOutputsData! + var onlyUseGen2Configuration = false + var useGen2Configuration: Bool { - ProcessInfo.processInfo.arguments.contains("GEN2") + ProcessInfo.processInfo.arguments.contains("GEN2") || onlyUseGen2Configuration } override func setUp() async throws { @@ -46,6 +48,7 @@ class AWSAuthBaseTest: XCTestCase { override func tearDown() async throws { try await super.tearDown() + subscription?.cancel() await Amplify.reset() } @@ -113,6 +116,84 @@ class AWSAuthBaseTest: XCTestCase { XCTFail("Amplify configuration failed") } } + + // Dictionary to store MFA codes with usernames as keys + var mfaCodeDictionary: [String: String] = [:] + var subscription: AmplifyAsyncThrowingSequence>? = nil + + let document: String = """ + subscription OnCreateMfaInfo { + onCreateMfaInfo { + username + code + expirationTime + } + } + """ + + /// Function to create a subscription and store MFA codes in a dictionary + func createMFASubscription() { + subscription = Amplify.API.subscribe(request: .init(document: document, responseType: [String: JSONValue].self)) + + // Create the subscription and listen for MFA code events + Task { + do { + guard let subscription = subscription else { return } + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + print("Subscription connect state is \(subscriptionConnectionState)") + case .data(let result): + switch result { + case .success(let mfaCodeResult): + print("Successfully got MFA code from subscription: \(mfaCodeResult)") + if let eventUsername = mfaCodeResult["onCreateMfaInfo"]?.asObject?["username"]?.stringValue, + let code = mfaCodeResult["onCreateMfaInfo"]?.asObject?["code"]?.stringValue { + // Store the code in the dictionary for the given username + mfaCodeDictionary[eventUsername] = code + } + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + print("Subscription terminated with error: \(error)") + } + } + } + + /// Test that waits for the MFA code using XCTestExpectation + func waitForMFACode(for username: String) async throws -> String? { + let expectation = XCTestExpectation(description: "Wait for MFA code") + expectation.expectedFulfillmentCount = 1 + + let task = Task { () -> String? in + var code: String? + for _ in 0..<30 { // Poll for the code, max 30 times (once per second) + if let mfaCode = mfaCodeDictionary[username] { + code = mfaCode + expectation.fulfill() // Fulfill the expectation when the value is found + break + } + try await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second + } + return code + } + + // Wait for expectation or timeout after 30 seconds + let result = await XCTWaiter.fulfillment(of: [expectation], timeout: 30) + + if result == .timedOut { + // Task cancels if timed out + task.cancel() + subscription?.cancel() + return nil + } + + subscription?.cancel() + return try await task.value + } } class TestConfigHelper { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift index 1dc43decc6..625f72c58a 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift @@ -22,10 +22,20 @@ enum AuthSignInHelper { password: String, email: String, phoneNumber: String? = nil) async throws -> Bool { + return try await signUpUserReturningResult(username: username, password: password, email: email, phoneNumber: phoneNumber).isSignUpComplete + } + + static func signUpUserReturningResult( + username: String, + password: String, + email: String? = nil, + phoneNumber: String? = nil) async throws -> AuthSignUpResult { + + var userAttributes: [AuthUserAttribute] = [] - var userAttributes = [ - AuthUserAttribute(.email, value: email) - ] + if let email = email { + userAttributes.append(AuthUserAttribute(.email, value: email)) + } if let phoneNumber = phoneNumber { userAttributes.append(AuthUserAttribute(.phoneNumber, value: phoneNumber)) @@ -34,7 +44,7 @@ enum AuthSignInHelper { let options = AuthSignUpRequest.Options( userAttributes: userAttributes) let result = try await Amplify.Auth.signUp(username: username, password: password, options: options) - return result.isSignUpComplete + return result } static func signInUser(username: String, password: String) async throws -> AuthSignInResult { @@ -46,7 +56,7 @@ enum AuthSignInHelper { password: String, email: String, phoneNumber: String? = nil) async throws -> Bool { - let signedUp = try await AuthSignInHelper.signUpUser( + let signedUp: Bool = try await AuthSignInHelper.signUpUser( username: username, password: password, email: email, diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift new file mode 100644 index 0000000000..b0f5e789d9 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -0,0 +1,69 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Amplify +import AWSCognitoAuthPlugin +import AWSAPIPlugin + +// MFA Required +// - Email +class EmailMFAOnlyTests: AWSAuthBaseTest { + + override func setUp() async throws { + onlyUseGen2Configuration = true + // Use a custom configuration these tests + amplifyOutputsFile = "testconfiguration/amplify_outputs" + + let awsApiPlugin = AWSAPIPlugin() + try Amplify.add(plugin: awsApiPlugin) + try await super.setUp() + AuthSessionHelper.clearSession() + } + + override func tearDown() async throws { + try await super.tearDown() + AuthSessionHelper.clearSession() + } + + /// Test a signIn with valid inputs getting continueSignInWithEmailMFASetup challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithEmailMFASetup response + /// + //Requires only Email MFA to be enabled + func disabled_testSuccessfulEmailMFASetupStep() async { + + do { + let uniqueId = UUID().uuidString + let username = "integTest\(uniqueId)" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password) + + let options = AuthSignInRequest.Options() + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: options) + + guard case .continueSignInWithEmailMFASetup = result.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup for next step, instead got: \(result.nextStep)") + return + } + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } +} diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift new file mode 100644 index 0000000000..f07079d45c --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift @@ -0,0 +1,272 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import Amplify +import AWSCognitoAuthPlugin +import AWSAPIPlugin + +// MFA Required +// - Email +// - TOTP +// - SMS +class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { + + override func setUp() async throws { + // run these tests only with Gen2 + onlyUseGen2Configuration = true + // Use a custom configuration these tests + amplifyOutputsFile = "testconfiguration/AWSCognitoAuthEmailMFAWithAllMFATypesRequired-amplify_outputs" + + let awsApiPlugin = AWSAPIPlugin() + try Amplify.add(plugin: awsApiPlugin) + try await super.setUp() + AuthSessionHelper.clearSession() + } + + override func tearDown() async throws { + try await super.tearDown() + AuthSessionHelper.clearSession() + } + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testSuccessfulMFASetupSelectionStep() async { + + let options = AuthSignInRequest.Options() + + do { + let uniqueId = UUID().uuidString + let username = "integTest\(uniqueId)" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password) + + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: options) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } + + /// Test a signIn with valid inputs getting confirmSignInWithEmailMFACode challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .confirmSignInWithEmailMFACode response + /// + func testSuccessfulEmailMFACodeStep() async { + + do { + createMFASubscription() + let uniqueId = UUID().uuidString + let username = "\(uniqueId)@integTest.com" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password, + email: username) + + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: AuthSignInRequest.Options()) + + guard case .confirmSignInWithEmailMFACode(let codeDetails) = result.nextStep else { + XCTFail("Result should be .confirmSignInWithEmailMFACode for next step, instead got: \(result.nextStep)") + return + } + if case .email(let destination) = codeDetails.destination { + XCTAssertNotNil(destination) + } else { + XCTFail("Destination should be email") + } + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // step 2: confirm sign in + guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { + XCTFail("failed to retrieve the mfa code") + return + } + + let confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: mfaCode, + options: .init()) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + } catch { + XCTFail("Received failure with error \(error)") + } + } + + + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testConfirmSignInForEmailMFASetupSelectionStep() async { + + do { + createMFASubscription() + let uniqueId = UUID().uuidString + let username = "\(uniqueId)" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password) + + // Step 1: initiate sign in + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: AuthSignInRequest.Options()) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 2: select email to continue setting up + var confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: MFAType.email.challengeResponse) + guard case .continueSignInWithEmailMFASetup = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + + // Step 3: pass an email to setup + confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: username + "@integTest.com") + guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + if case .email(let destination) = deliveryDetails.destination { + XCTAssertNotNil(destination) + } else { + XCTFail("Destination should be email") + } + + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // step 4: confirm sign in + guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { + XCTFail("failed to retrieve the mfa code") + return + } + confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: mfaCode, + options: .init()) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + + } catch { + XCTFail("Received failure with error \(error)") + } + } + + /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge + /// + /// - Given: Given an auth plugin with mocked service. + /// + /// - When: + /// - I invoke signIn with valid values + /// - Then: + /// - I should get a .continueSignInWithMFASetupSelection response + /// + func testConfirmSignInForTOTPMFASetupSelectionStep() async { + do { + + let uniqueId = UUID().uuidString + let username = "\(uniqueId)" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password) + + // Step 1: initiate sign in + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: AuthSignInRequest.Options()) + guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { + XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + return + } + XCTAssertTrue(mfaTypes.contains(.totp)) + XCTAssertTrue(mfaTypes.contains(.email)) + XCTAssertFalse(mfaTypes.contains(.sms)) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 2: continue sign in by selecting TOTP for set up + var confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: MFAType.totp.challengeResponse) + guard case .continueSignInWithTOTPSetup(let totpDetails) = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + XCTAssertNotNil(totpDetails.sharedSecret) + XCTAssertNotNil(totpDetails.username) + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // Step 3: complete sign in by verifying TOTP set up + let totpCode = TOTPHelper.generateTOTPCode(sharedSecret: totpDetails.sharedSecret) + let pluginOptions = AWSAuthConfirmSignInOptions(friendlyDeviceName: "device") + confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: totpCode, + options: .init(pluginOptions: pluginOptions)) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + + } catch { + XCTFail("Received failure with error \(error)") + } + } +} From 44a1171a5bfd8004d7195c22a8478682ee63a9c0 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Wed, 2 Oct 2024 21:48:29 -0400 Subject: [PATCH 05/10] integration tests wave 2 --- .../AuthHostApp.xcodeproj/project.pbxproj | 7 +++ .../EmailMFATests/EmailMFAOnlyTests.swift | 48 +++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj index 14a1cccaff..193420021f 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ 487C403F2CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; 487C40402CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; 487C40412CADE8DA009CF221 /* EmailMFAOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C403E2CADE88F009CF221 /* EmailMFAOnlyTests.swift */; }; + 487C40432CAE2905009CF221 /* AWSAPIPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 487C40422CAE2905009CF221 /* AWSAPIPlugin */; }; 48916F382A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F372A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift */; }; 48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F392A412CEE00E3E1B1 /* TOTPHelper.swift */; }; 48916F3C2A42333E00E3E1B1 /* MFAPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F3B2A42333E00E3E1B1 /* MFAPreferenceTests.swift */; }; @@ -258,6 +259,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 487C40432CAE2905009CF221 /* AWSAPIPlugin in Frameworks */, 681B769A2A3CBA97004B59D9 /* Amplify in Frameworks */, 681B769C2A3CBA97004B59D9 /* AWSCognitoAuthPlugin in Frameworks */, ); @@ -598,6 +600,7 @@ packageProductDependencies = ( 681B76992A3CBA97004B59D9 /* Amplify */, 681B769B2A3CBA97004B59D9 /* AWSCognitoAuthPlugin */, + 487C40422CAE2905009CF221 /* AWSAPIPlugin */, ); productName = "AuthWatchApp Watch App"; productReference = 681B76802A3CB86B004B59D9 /* AuthWatchApp.app */; @@ -1510,6 +1513,10 @@ isa = XCSwiftPackageProductDependency; productName = AWSAPIPlugin; }; + 487C40422CAE2905009CF221 /* AWSAPIPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSAPIPlugin; + }; 681B76992A3CBA97004B59D9 /* Amplify */ = { isa = XCSwiftPackageProductDependency; productName = Amplify; diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift index b0f5e789d9..3ace817910 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -12,12 +12,12 @@ import AWSAPIPlugin // MFA Required // - Email -class EmailMFAOnlyTests: AWSAuthBaseTest { +class EmailMFARequiredTests: AWSAuthBaseTest { override func setUp() async throws { onlyUseGen2Configuration = true // Use a custom configuration these tests - amplifyOutputsFile = "testconfiguration/amplify_outputs" + amplifyOutputsFile = "testconfiguration/AWSCognitoEmailMFARequiredTests-amplify_outputs" let awsApiPlugin = AWSAPIPlugin() try Amplify.add(plugin: awsApiPlugin) @@ -38,15 +38,20 @@ class EmailMFAOnlyTests: AWSAuthBaseTest { /// - I invoke signIn with valid values /// - Then: /// - I should get a .continueSignInWithEmailMFASetup response - /// + /// - When: + /// - I invoke confirm signIn with valid values, + /// - Then: + /// - With series of challenges, sign in should succeed //Requires only Email MFA to be enabled - func disabled_testSuccessfulEmailMFASetupStep() async { + func testSuccessfulEmailMFASetupStep() async { do { + createMFASubscription() + let uniqueId = UUID().uuidString let username = "integTest\(uniqueId)" let password = "Pp123@\(uniqueId)" - + _ = try await AuthSignInHelper.signUpUserReturningResult( username: username, password: password) @@ -61,7 +66,40 @@ class EmailMFAOnlyTests: AWSAuthBaseTest { XCTFail("Result should be .continueSignInWithEmailMFASetup for next step, instead got: \(result.nextStep)") return } + + // Step 2: pass an email to setup + var confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: username + "@integTest.com") + guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { + XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + return + } + if case .email(let destination) = deliveryDetails.destination { + XCTAssertNotNil(destination) + } else { + XCTFail("Destination should be email") + } + + XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // step 3: confirm sign in + guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { + XCTFail("failed to retrieve the mfa code") + return + } + confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: mfaCode, + options: .init()) + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Result should be .done for next step") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + + // email should get added to the account + let attributes = try await Amplify.Auth.fetchUserAttributes() + XCTAssertEqual(attributes.first(where: { $0.key == .email})?.value, username + "@integTest.com") } catch { XCTFail("Received failure with error \(error)") } From 34fa4561b64e5a7c684043ac78877f589b6cdc9e Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:57:53 -0400 Subject: [PATCH 06/10] integration tests wave 3 --- .../EmailMFATests/EmailMFAOnlyTests.swift | 73 +++++---- ...EmailMFAWithAllMFATypesRequiredTests.swift | 145 +++++++++--------- 2 files changed, 118 insertions(+), 100 deletions(-) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift index 3ace817910..ffbfa5f367 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -10,98 +10,111 @@ import Amplify import AWSCognitoAuthPlugin import AWSAPIPlugin -// MFA Required -// - Email +// Test class for scenarios where only Email MFA is required. +// - This test suite verifies the sign-in process when only Email MFA is enabled. class EmailMFARequiredTests: AWSAuthBaseTest { + // Sets up the test environment with a custom configuration and adds required plugins override func setUp() async throws { + // Only run these tests with Gen2 configuration onlyUseGen2Configuration = true - // Use a custom configuration these tests + + // Specify a custom test configuration for these tests amplifyOutputsFile = "testconfiguration/AWSCognitoEmailMFARequiredTests-amplify_outputs" + // Add API plugin to Amplify let awsApiPlugin = AWSAPIPlugin() try Amplify.add(plugin: awsApiPlugin) try await super.setUp() + + // Clear session to ensure a fresh state for each test AuthSessionHelper.clearSession() } + // Tear down the test environment and clear the session override func tearDown() async throws { try await super.tearDown() AuthSessionHelper.clearSession() } - /// Test a signIn with valid inputs getting continueSignInWithEmailMFASetup challenge + /// Test the sign-in flow when Email MFA setup is required. /// - /// - Given: Given an auth plugin with mocked service. + /// - Given: A new user is created, and only Email MFA is required for the account. + /// - When: The user provides valid username and password, and then proceeds through the MFA setup flow. + /// - Then: The user should successfully complete the MFA setup and be able to sign in. /// - /// - When: - /// - I invoke signIn with valid values - /// - Then: - /// - I should get a .continueSignInWithEmailMFASetup response - /// - When: - /// - I invoke confirm signIn with valid values, - /// - Then: - /// - With series of challenges, sign in should succeed - //Requires only Email MFA to be enabled + /// - MFA Setup Flow: + /// - Step 1: User signs in and receives the `continueSignInWithEmailMFASetup` challenge. + /// - Step 2: User provides their email for MFA setup. + /// - Step 3: User receives and confirms the MFA code sent to their email. + /// - Step 4: Sign-in completes, and the email is associated with the user account. func testSuccessfulEmailMFASetupStep() async { - do { + // Step 1: Set up a subscription to receive MFA codes createMFASubscription() + // Step 2: Sign up a new user let uniqueId = UUID().uuidString let username = "integTest\(uniqueId)" let password = "Pp123@\(uniqueId)" - + _ = try await AuthSignInHelper.signUpUserReturningResult( username: username, password: password) let options = AuthSignInRequest.Options() + // Step 3: Initiate sign-in, expecting MFA setup to be required let result = try await Amplify.Auth.signIn( username: username, password: password, options: options) + // Step 4: Ensure that the next step is to set up Email MFA guard case .continueSignInWithEmailMFASetup = result.nextStep else { - XCTFail("Result should be .continueSignInWithEmailMFASetup for next step, instead got: \(result.nextStep)") + XCTFail("Expected .continueSignInWithEmailMFASetup step, got \(result.nextStep)") return } - // Step 2: pass an email to setup + // Step 5: Provide the email address to complete MFA setup var confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: username + "@integTest.com") + + // Step 6: Ensure that the next step is to confirm the Email MFA code guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { - XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(confirmSignInResult.nextStep)") return } if case .email(let destination) = deliveryDetails.destination { - XCTAssertNotNil(destination) + XCTAssertNotNil(destination, "Email destination should be provided") } else { - XCTFail("Destination should be email") + XCTFail("Expected the destination to be email") } - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") - // step 3: confirm sign in + // Step 7: Retrieve the MFA code sent to the email and confirm the sign-in guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { - XCTFail("failed to retrieve the mfa code") + XCTFail("Failed to retrieve the MFA code") return } + confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: mfaCode, options: .init()) + + // Step 8: Ensure that the sign-in process is complete guard case .done = confirmSignInResult.nextStep else { - XCTFail("Result should be .done for next step") + XCTFail("Expected .done step after confirming MFA") return } - XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertTrue(confirmSignInResult.isSignedIn, "User should be signed in at this stage") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at the initial stage") - // email should get added to the account + // Step 9: Verify that the email is associated with the user account let attributes = try await Amplify.Auth.fetchUserAttributes() - XCTAssertEqual(attributes.first(where: { $0.key == .email})?.value, username + "@integTest.com") + XCTAssertEqual(attributes.first(where: { $0.key == .email })?.value, username + "@integTest.com") } catch { - XCTFail("Received failure with error \(error)") + XCTFail("Unexpected error: \(error)") } } } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift index f07079d45c..3340d6a682 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift @@ -10,43 +10,44 @@ import Amplify import AWSCognitoAuthPlugin import AWSAPIPlugin -// MFA Required -// - Email -// - TOTP -// - SMS +// Test class for MFA Required scenario with Email, TOTP, and SMS MFA enabled. +// - This test suite verifies various steps in the MFA sign-in process when multiple MFA types (Email, TOTP, SMS) are required. class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { + // Sets up the test environment using Gen2 configuration and adds required plugins override func setUp() async throws { - // run these tests only with Gen2 + // Only run these tests with Gen2 configuration onlyUseGen2Configuration = true - // Use a custom configuration these tests + + // Specify a custom test configuration for these tests amplifyOutputsFile = "testconfiguration/AWSCognitoAuthEmailMFAWithAllMFATypesRequired-amplify_outputs" + // Add API plugin to Amplify let awsApiPlugin = AWSAPIPlugin() try Amplify.add(plugin: awsApiPlugin) try await super.setUp() + + // Clear session to ensure a fresh state for each test AuthSessionHelper.clearSession() } + // Tear down test environment and clear the session override func tearDown() async throws { try await super.tearDown() AuthSessionHelper.clearSession() } - /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge - /// - /// - Given: Given an auth plugin with mocked service. - /// - /// - When: - /// - I invoke signIn with valid values - /// - Then: - /// - I should get a .continueSignInWithMFASetupSelection response + /// Test the sign-in flow when MFA setup is required with multiple MFA options (Email and TOTP). /// + /// - Given: The user has successfully signed up and is trying to sign in. + /// - When: The user provides valid username and password. + /// - Then: The sign-in process should return a `.continueSignInWithMFASetupSelection` challenge to select the MFA type to set up. func testSuccessfulMFASetupSelectionStep() async { let options = AuthSignInRequest.Options() do { + // Step 1: Sign up a new user let uniqueId = UUID().uuidString let username = "integTest\(uniqueId)" let password = "Pp123@\(uniqueId)" @@ -55,156 +56,159 @@ class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { username: username, password: password) + // Step 2: Attempt to sign in with the newly created user let result = try await Amplify.Auth.signIn( username: username, password: password, options: options) + + // Step 3: Ensure that MFA setup is required and TOTP and Email are available as options guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { - XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + XCTFail("Expected .continueSignInWithMFASetupSelection step") return } - XCTAssertTrue(mfaTypes.contains(.totp)) - XCTAssertTrue(mfaTypes.contains(.email)) - XCTAssertFalse(mfaTypes.contains(.sms)) - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertTrue(mfaTypes.contains(.totp), "TOTP should be available as an MFA option") + XCTAssertTrue(mfaTypes.contains(.email), "Email should be available as an MFA option") + XCTAssertFalse(mfaTypes.contains(.sms), "SMS should not be available as an MFA option") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") } catch { - XCTFail("Received failure with error \(error)") + XCTFail("Unexpected error: \(error)") } } - /// Test a signIn with valid inputs getting confirmSignInWithEmailMFACode challenge - /// - /// - Given: Given an auth plugin with mocked service. - /// - /// - When: - /// - I invoke signIn with valid values - /// - Then: - /// - I should get a .confirmSignInWithEmailMFACode response + /// Test the sign-in flow with Email MFA when the user is prompted to confirm the MFA code. /// + /// - Given: The user is required to provide an Email MFA code to complete sign-in. + /// - When: The user provides valid username and password, and then submits the correct MFA code. + /// - Then: The sign-in should complete after confirming the MFA code. func testSuccessfulEmailMFACodeStep() async { - do { + // Step 1: Set up a subscription to receive MFA codes createMFASubscription() let uniqueId = UUID().uuidString let username = "\(uniqueId)@integTest.com" let password = "Pp123@\(uniqueId)" + // Step 2: Sign up a new user with email _ = try await AuthSignInHelper.signUpUserReturningResult( username: username, password: password, email: username) + // Step 3: Attempt to sign in, which should prompt for Email MFA let result = try await Amplify.Auth.signIn( username: username, password: password, options: AuthSignInRequest.Options()) + // Step 4: Verify that the next step is to confirm the Email MFA code guard case .confirmSignInWithEmailMFACode(let codeDetails) = result.nextStep else { - XCTFail("Result should be .confirmSignInWithEmailMFACode for next step, instead got: \(result.nextStep)") + XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(result.nextStep)") return } if case .email(let destination) = codeDetails.destination { - XCTAssertNotNil(destination) + XCTAssertNotNil(destination, "Email destination should be provided") } else { XCTFail("Destination should be email") } - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") - // step 2: confirm sign in + // Step 5: Retrieve the MFA code and confirm the sign-in guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { - XCTFail("failed to retrieve the mfa code") + XCTFail("Failed to retrieve the MFA code") return } let confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: mfaCode, options: .init()) + + // Step 6: Ensure that the sign-in is complete guard case .done = confirmSignInResult.nextStep else { - XCTFail("Result should be .done for next step") + XCTFail("Expected .done step after confirming MFA") return } - XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + XCTAssertTrue(confirmSignInResult.isSignedIn, "User should be signed in at this stage") } catch { - XCTFail("Received failure with error \(error)") + XCTFail("Unexpected error: \(error)") } } - - - /// Test a signIn with valid inputs getting continueSignInWithMFASetupSelection challenge - /// - /// - Given: Given an auth plugin with mocked service. - /// - /// - When: - /// - I invoke signIn with valid values - /// - Then: - /// - I should get a .continueSignInWithMFASetupSelection response + /// Test confirming sign-in for Email MFA setup after selecting it as an MFA option. /// + /// - Given: The user is prompted to select Email as an MFA type. + /// - When: The user selects Email and submits their email address for setup. + /// - Then: The user should be prompted to confirm the Email MFA code and complete sign-in. func testConfirmSignInForEmailMFASetupSelectionStep() async { - do { + // Step 1: Set up a subscription to receive MFA codes createMFASubscription() let uniqueId = UUID().uuidString let username = "\(uniqueId)" let password = "Pp123@\(uniqueId)" + // Step 2: Sign up a new user _ = try await AuthSignInHelper.signUpUserReturningResult( username: username, password: password) - // Step 1: initiate sign in + // Step 3: Initiate sign-in, expecting MFA setup selection let result = try await Amplify.Auth.signIn( username: username, password: password, options: AuthSignInRequest.Options()) + + // Step 4: Verify that the next step is to select an MFA type guard case .continueSignInWithMFASetupSelection(let mfaTypes) = result.nextStep else { - XCTFail("Result should be .continueSignInWithMFASetupSelection for next step") + XCTFail("Expected .continueSignInWithMFASetupSelection step") return } - XCTAssertTrue(mfaTypes.contains(.totp)) - XCTAssertTrue(mfaTypes.contains(.email)) - XCTAssertFalse(mfaTypes.contains(.sms)) - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertTrue(mfaTypes.contains(.totp), "TOTP should be available as an MFA option") + XCTAssertTrue(mfaTypes.contains(.email), "Email should be available as an MFA option") + XCTAssertFalse(mfaTypes.contains(.sms), "SMS should not be available as an MFA option") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") - // Step 2: select email to continue setting up + // Step 5: Select Email as the MFA option to proceed var confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: MFAType.email.challengeResponse) + + // Step 6: Verify that the next step is to set up Email MFA guard case .continueSignInWithEmailMFASetup = confirmSignInResult.nextStep else { - XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + XCTFail("Expected .continueSignInWithEmailMFASetup step") return } - // Step 3: pass an email to setup + // Step 7: Provide the email address to complete the setup confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: username + "@integTest.com") + + // Step 8: Verify that the next step is to confirm the Email MFA code guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { - XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") + XCTFail("Expected .confirmSignInWithEmailMFACode step") return } if case .email(let destination) = deliveryDetails.destination { - XCTAssertNotNil(destination) - } else { - XCTFail("Destination should be email") + XCTAssertNotNil(destination, "Email destination should be provided") } - XCTAssertFalse(result.isSignedIn, "Signin result should be complete") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") - // step 4: confirm sign in + // Step 9: Confirm the sign-in with the received MFA code guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { - XCTFail("failed to retrieve the mfa code") + XCTFail("Failed to retrieve the MFA code") return } confirmSignInResult = try await Amplify.Auth.confirmSignIn( challengeResponse: mfaCode, options: .init()) guard case .done = confirmSignInResult.nextStep else { - XCTFail("Result should be .done for next step") + XCTFail("Expected .done step after confirming MFA") return } - XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + XCTAssertTrue(confirmSignInResult.isSignedIn, "User should be signed in at this stage") } catch { - XCTFail("Received failure with error \(error)") + XCTFail("Unexpected error: \(error)") } } @@ -260,13 +264,14 @@ class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { challengeResponse: totpCode, options: .init(pluginOptions: pluginOptions)) guard case .done = confirmSignInResult.nextStep else { - XCTFail("Result should be .done for next step") + XCTFail("Expected .done step after confirming MFA") return } - XCTAssertTrue(confirmSignInResult.isSignedIn, "Signin result should NOT be complete") + XCTAssertTrue(confirmSignInResult.isSignedIn, "User should be signed in at this stage") } catch { - XCTFail("Received failure with error \(error)") + XCTFail("Unexpected error: \(error)") } } + } From b760c71801aa67207810de460a2e38c51525f0c5 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:24:01 -0400 Subject: [PATCH 07/10] Add test setup instructions wave 4 --- .../EmailMFATests/EmailMFAOnlyTests.swift | 9 + ...EmailMFAWithAllMFATypesRequiredTests.swift | 10 + .../MFATests/EmailMFATests/README.md | 424 ++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift index ffbfa5f367..ee03d8ad2f 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -10,8 +10,17 @@ import Amplify import AWSCognitoAuthPlugin import AWSAPIPlugin +// Follow MFATests/EmailMFAOnlyTests/Readme.md for test setup locally // Test class for scenarios where only Email MFA is required. // - This test suite verifies the sign-in process when only Email MFA is enabled. +// loginWith: { +// email: true, +// }, +// multifactor: { +// mode: "REQUIRED", +// sms: true, +// email: true, (email has not been added to backend at the time of writing this test) +// }, class EmailMFARequiredTests: AWSAuthBaseTest { // Sets up the test environment with a custom configuration and adds required plugins diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift index 3340d6a682..8b6dc32616 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift @@ -10,8 +10,18 @@ import Amplify import AWSCognitoAuthPlugin import AWSAPIPlugin +// Follow MFATests/EmailMFAOnlyTests/Readme.md for test setup locally // Test class for MFA Required scenario with Email, TOTP, and SMS MFA enabled. // - This test suite verifies various steps in the MFA sign-in process when multiple MFA types (Email, TOTP, SMS) are required. +// loginWith: { +// email: true, +// }, +// multifactor: { +// mode: "REQUIRED", +// sms: true, +// totp: true, +// email: true, (email has not been added to backend at the time of writing this test) +// }, class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { // Sets up the test environment using Gen2 configuration and adds required plugins diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md new file mode 100644 index 0000000000..c29d9f11e4 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md @@ -0,0 +1,424 @@ +# Schema: AuthIntegrationTests - AWSCognitoAuthPlugin Integration tests + +The following steps demonstrate how to setup the integration tests for auth plugin where an OTP is sent to the user's email address or phone number. T + +## Schema: AuthGen2IntegrationTests + +The following steps demonstrate how to setup the integration tests for auth plugin using Amplify CLI (Gen2). + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@latest`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.3", + "typescript": "^5.4.5" + }, + "dependencies": { + "aws-amplify": "^6.2.0" + }, +} +``` + +2. Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from "@aws-amplify/backend"; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true, + }, + multifactor: { + mode: "REQUIRED", + sms: true, + }, + userAttributes: { + email: { + required: false, + mutable: true, + }, + phoneNumber: { + required: false, + mutable: true, + }, + }, + accountRecovery: "NONE", + senders: { + email: { + fromEmail, + }, + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: "./pre-sign-up-handler.ts", + }), + }, +}); +``` + +```ts +import type { PreSignUpTriggerHandler } from "aws-lambda"; + +export const handler: PreSignUpTriggerHandler = async (event) => { + event.response.autoConfirmUser = true; // Automatically confirm the user + + // Automatically mark the user's email as verified + if (event.request.userAttributes.hasOwnProperty("email")) { + event.response.autoVerifyEmail = true; // Automatically verify the email + } + + // Automatically mark the user's phone number as verified + if (event.request.userAttributes.hasOwnProperty("phone_number")) { + event.response.autoVerifyPhone = true; // Automatically verify the phone number + } + // Return to Amazon Cognito + return event; +}; +``` + +Update `amplify/data/mfa/index.ts`. The resulting file should look like this + +```ts +import { Duration, Expiration, RemovalPolicy, Stack } from "aws-cdk-lib"; +import { + Assign, + AuthorizationType, + FieldLogLevel, + GraphqlApi, + MappingTemplate, + PrimaryKey, + SchemaFile, + Values, +} from "aws-cdk-lib/aws-appsync"; +import { Table, BillingMode, AttributeType } from "aws-cdk-lib/aws-dynamodb"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * It creates AppSync and Dynamo resources using CDK + * + * *Note: It was not possible to use gen2 to create data resources due to a circular dependency error while + * deploying resources.* + * + * A circular dependency is when, + * + * - a resource that is being deployed depends on another resource that is being deployed and vice-versa. + * - or a resource depends on its own resource. + * + * For instance, + * + * Auth resources -> Data resources -> Auth resources + * + * Reference: https://aws.amazon.com/blogs/infrastructure-and-automation/handling-circular-dependency-errors-in-aws-cloudformation/ + * + */ +export function createMfaInfoGraphqlApi(stack: Stack): GraphqlApi { + const authorizationType = AuthorizationType.API_KEY; + const resolvedPath = path.resolve(__dirname, "index.graphql"); + const graphqlapi = new GraphqlApi(stack, "MfaInfoGraphqlApi", { + name: "MfaInfoGraphql", + definition: { + schema: SchemaFile.fromAsset(resolvedPath), + }, + authorizationConfig: { + defaultAuthorization: { + authorizationType, + apiKeyConfig: { + expires: Expiration.after(Duration.days(365)), + }, + }, + }, + logConfig: { + fieldLogLevel: FieldLogLevel.ALL, + excludeVerboseContent: false, + }, + }); + + const mfaCodesTable = new Table(stack, `MfaInfoTable`, { + removalPolicy: RemovalPolicy.DESTROY, + billingMode: BillingMode.PAY_PER_REQUEST, + partitionKey: { + type: AttributeType.STRING, + name: "username", + }, + sortKey: { + type: AttributeType.STRING, + name: "code", + }, + timeToLiveAttribute: "expirationTime", + }); + + const mfaCodesSource = graphqlapi.addDynamoDbDataSource( + "GraphQLApiMFACodes", + mfaCodesTable + ); + // Mutation.createMfaInfo + mfaCodesSource.createResolver(`MutationCreateMFACodeResolver`, { + typeName: "Mutation", + fieldName: "createMfaInfo", + requestMappingTemplate: MappingTemplate.dynamoDbPutItem( + new PrimaryKey( + new Assign("username", "$input.username"), + new Assign("code", "$input.code") + ), + Values.projecting("input") + ), + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), + }); + + // Query.listMFACodes + mfaCodesSource.createResolver(`QueryListMfaInfoResolver`, { + typeName: "Query", + fieldName: "listMfaInfo", + requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), + }); + + return graphqlapi; +} +``` + +Update `backend.ts` + +```ts +import { defineBackend } from "@aws-amplify/backend"; +import { auth } from "./auth/resource"; +import { Key } from "aws-cdk-lib/aws-kms"; +import { RemovalPolicy } from "aws-cdk-lib"; +import { createMfaInfoGraphqlApi } from "./data/mfaInfo"; +import { senderFactory } from "./helpers"; + +enum LambdaEnvKeys { + GRAPHQL_API_ENDPOINT = "GRAPHQL_API_ENDPOINT", + GRAPHQL_API_KEY = "GRAPHQL_API_KEY", + KMS_KEY_ARN = "KMS_KEY_ARN", +} + +const backend = defineBackend({ + auth, +}); + +const { cfnResources, userPool } = backend.auth.resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// an empty array denotes "email" and "phone_number" cannot be used as a username +cfnUserPool.usernameAttributes = []; + +// Create data resources +const mfaInfoGraphqlApi = createMfaInfoGraphqlApi(userPool.stack); +// Create kms resources +const customSenderKmsKey = new Key(stack, "CustomSenderKmsKey", { + description: `Key for encrypting/decrypting messages`, + removalPolicy: RemovalPolicy.DESTROY, +}); +// Create Cognito senders +const environment = { + [LambdaEnvKeys.GRAPHQL_API_ENDPOINT]: mfaInfoGraphqlApi.graphqlUrl, + [LambdaEnvKeys.GRAPHQL_API_KEY]: mfaInfoGraphqlApi.apiKey ?? "", + [LambdaEnvKeys.KMS_KEY_ARN]: customSenderKmsKey.keyArn, +}; +const cognitoSender = senderFactory( + stack, + mfaInfoGraphqlApi, + customSenderKmsKey, + cfnUserPool +); +const customEmailSender = cognitoSender("email-sender", environment); +const customSmsSender = cognitoSender("sms-sender", environment); + +// Configure the user pool to use the custom senders +cfnUserPool.lambdaConfig = { + customEmailSender: { + lambdaArn: customEmailSender.functionArn, + lambdaVersion: "V1_0", + }, + customSmsSender: { + lambdaArn: customSmsSender.functionArn, + lambdaVersion: "V1_0", + }, + kmsKeyId: customSenderKmsKey.keyArn, +}; + +// Add data resources output. +// Gen2 won't be able to auto generate data output as data resources were generated by CDK. +backend.addOutput({ + data: { + aws_region: stack.region, + url: mfaInfoGraphqlApi.graphqlUrl, + api_key: mfaInfoGraphqlApi.apiKey, + default_authorization_type: "API_KEY", + authorization_types: [], + }, +}); + +// Enable Device Tracking +// https://docs.amplify.aws/react/build-a-backend/auth/concepts/multi-factor-authentication/#remember-a-device + +cfnUserPool.addPropertyOverride("DeviceConfiguration", { + ChallengeRequiredOnNewDevice: true, + DeviceOnlyRememberedOnUserPrompt: false, +}); +``` + +The triggers should look as follows: + +Common + +```ts +// Code adapted from: +// - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html#code-examples +// - https://github.com/aws-samples/amazon-cognito-user-pool-development-and-testing-with-sms-redirected-to-email + +import { + buildClient, + CommitmentPolicy, + KmsKeyringNode, +} from "@aws-crypto/client-node"; + +const { decrypt } = buildClient(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT); + +/** + * Decrypts `code` using the KMS keyring provided by the environment. + * @param code The encrypted code sent from Cognito. + * @returns The plaintext (decrypted) code. + */ +const decryptCode = async (code: string): Promise => { + const { KMS_KEY_ARN } = process.env; + const keyring = new KmsKeyringNode({ + keyIds: [KMS_KEY_ARN!], + }); + const { plaintext } = await decrypt(keyring, Buffer.from(code, "base64")); + return plaintext.toString("ascii"); +}; + +/** + * Decrypts and broadcasts `code` to the AppSync endpoint provided by the environment. + * @param code The encrypted code sent from Cognito. + */ +export const decryptAndBroadcastCode = async ( + username: string, + code: string +): Promise => { + const { GRAPHQL_API_ENDPOINT, GRAPHQL_API_KEY } = process.env; + const plaintextCode = await decryptCode(code); + console.log(`Got MFA code for username ${username}: ${plaintextCode}`); + const EXPIRATION_TIME_IN_SECONDS = 1 * 60 * 1000; // 1 minute; + try { + const resp = await fetch(GRAPHQL_API_ENDPOINT!, { + method: "POST", + headers: { + "x-api-key": GRAPHQL_API_KEY!, + }, + body: JSON.stringify({ + query: ` + mutation CreateMfaInfo($username: String!, $code: String! $expirationTime: AWSTimestamp!) { + createMfaInfo(input: { + username: $username + code: $code + expirationTime: $expirationTime + }) { + username + code + expirationTime + } + } + `, + variables: { + username, + code: plaintextCode, + expirationTime: + Math.floor(Date.now() / 1000) + EXPIRATION_TIME_IN_SECONDS, + }, + }), + }); + const json = await resp.json(); + console.log(`Got GraphQL response: ${JSON.stringify(json, null, 2)}`); + } catch (error) { + console.error("Could not POST to GraphQL endpoint: ", error); + } +}; +``` + +custom-email-sender + +```ts +import { CustomEmailSenderTriggerHandler } from "aws-lambda"; +import { decryptAndBroadcastCode } from "./common"; + +export const handler: CustomEmailSenderTriggerHandler = async (event) => { + console.log(`Got event: ${JSON.stringify(event, null, 2)}`); + + if ( + event.triggerSource === "CustomEmailSender_AdminCreateUser" || + event.triggerSource == "CustomEmailSender_AccountTakeOverNotification" + ) { + console.warn(`Not handling trigger source: ${event.triggerSource}`); + return event; + } + + const { userName } = event; + const { code } = event.request; + + await decryptAndBroadcastCode(userName, code!); + + return event; +}; +``` + +custom-sms-sender + +```ts +import { CustomSMSSenderTriggerHandler } from "aws-lambda"; +import { decryptAndBroadcastCode } from "./common"; + +export const handler: CustomSMSSenderTriggerHandler = async (event) => { + console.log(`Got event: ${JSON.stringify(event, null, 2)}`); + + if (event.triggerSource === "CustomSMSSender_AdminCreateUser") { + console.warn(`Not handling trigger source: ${event.triggerSource}`); + return event; + } + + const { userName } = event; + const { code } = event.request; + + await decryptAndBroadcastCode(userName, code!); + + return event; +}; +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx ampx sandbox --identifier mfa-req-email --outputs-out-dir amplify_outputs/mfa-req-email +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `XYZ-amplify_outputs.json` (replace xyz with the name of the file your test is expecting). The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/XYZ-amplify_outputs.json +``` \ No newline at end of file From 8b163068ad86d29d4378cf95617e930b2248987a Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:38:54 -0400 Subject: [PATCH 08/10] Add edge case --- .../EmailMFATests/EmailMFAOnlyTests.swift | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift index ee03d8ad2f..bac8447467 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -126,4 +126,94 @@ class EmailMFARequiredTests: AWSAuthBaseTest { XCTFail("Unexpected error: \(error)") } } + + + /// Test the sign-in flow when an incorrect MFA code is entered first, followed by the correct MFA code. + /// + /// - Given: A new user is created, and only Email MFA is required for the account. + /// - When: The user provides valid username and password, receives the MFA code via email, enters an incorrect code, + /// and then enters the correct MFA code. + /// - Then: The user should receive a `codeMismatch` error for the incorrect code, but after entering the correct MFA code, + /// they should successfully complete the MFA process and sign in. + /// + /// - MFA Setup Flow: + /// - Step 1: User signs in and receives the `confirmSignInWithEmailMFACode` challenge. + /// - Step 2: User enters an incorrect MFA code and receives a `codeMismatch` error. + /// - Step 3: User enters the correct MFA code. + /// - Step 4: Sign-in completes, and the email is associated with the user account. + func testSuccessfulEmailMFAWithIncorrectCodeFirstAndThenValidOne() async { + do { + // Step 1: Set up a subscription to receive MFA codes + createMFASubscription() + + // Step 2: Sign up a new user + let uniqueId = UUID().uuidString + let username = "integTest\(uniqueId)" + let password = "Pp123@\(uniqueId)" + + _ = try await AuthSignInHelper.signUpUserReturningResult( + username: username, + password: password, + email: username + "@integTest.com") + + let options = AuthSignInRequest.Options() + + // Step 3: Initiate sign-in, expecting MFA setup to be required + let result = try await Amplify.Auth.signIn( + username: username, + password: password, + options: options) + + // Step 6: Ensure that the next step is to confirm the Email MFA code + guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = result.nextStep else { + XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(result.nextStep)") + return + } + if case .email(let destination) = deliveryDetails.destination { + XCTAssertNotNil(destination, "Email destination should be provided") + } else { + XCTFail("Expected the destination to be email") + } + + XCTAssertFalse(result.isSignedIn, "User should not be signed in at this stage") + + // Step 7: Retrieve the MFA code sent to the email and confirm the sign-in + guard let mfaCode = try await waitForMFACode(for: username.lowercased()) else { + XCTFail("Failed to retrieve the MFA code") + return + } + + // Step 6: Enter an incorrect MFA code first + do { + _ = try await Amplify.Auth.confirmSignIn( + challengeResponse: "000000", + options: .init()) + } catch AuthError.service(_, _, let error) { + + guard let underlyingError = error as? AWSCognitoAuthError else { + XCTFail("Expected an AWS Cognito Auth error") + return + } + guard underlyingError == .codeMismatch else { + XCTFail("Expected .codeMismatch error") + return + } + + // Step 7: Enter the correct MFA code + let confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: mfaCode, + options: .init()) + + // Step 8: Ensure that the sign-in process is complete + guard case .done = confirmSignInResult.nextStep else { + XCTFail("Expected .done step after confirming MFA") + return + } + XCTAssertTrue(confirmSignInResult.isSignedIn, "User should be signed in at this stage") + XCTAssertFalse(result.isSignedIn, "User should not be signed in at the initial stage") + } + } catch { + XCTFail("Unexpected error: \(error)") + } + } } From 27112dff61c01d9cf35fd26aac9cd3927ffda7f2 Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:32:25 -0400 Subject: [PATCH 09/10] update readme to include graphQL details --- .../MFATests/EmailMFATests/README.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md index c29d9f11e4..b2a4c2fa45 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/README.md @@ -92,6 +92,36 @@ export const handler: PreSignUpTriggerHandler = async (event) => { return event; }; ``` +Create a file `amplify/data/mfa/index.graphql` with the following content + +```graphql +# A Graphql Schema for creating Mfa info such as code and username. + +type Query { + listMfaInfo: [MfaInfo] @aws_api_key +} + +type Mutation { + createMfaInfo(input: CreateMfaInfoInput!): MfaInfo @aws_api_key +} + +type Subscription { + onCreateMfaInfo(username: String): MfaInfo + @aws_subscribe(mutations: ["createMfaInfo"]) +} + +input CreateMfaInfoInput { + username: String! + code: String! + expirationTime: AWSTimestamp! +} + +type MfaInfo { + username: String! + code: String! + expirationTime: AWSTimestamp! +} +``` Update `amplify/data/mfa/index.ts`. The resulting file should look like this From f332d126512fd9e28cbe07f4b3fdb36ab9cff4cb Mon Sep 17 00:00:00 2001 From: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:33:32 -0400 Subject: [PATCH 10/10] update emailMFACode step to confirmSignInWithOTP --- Amplify/Categories/Auth/Models/AuthSignInStep.swift | 10 +++++----- .../Actions/SignIn/InitializeResolveChallenge.swift | 2 +- .../ClientBehaviorTests/SignIn/EmailMFATests.swift | 10 +++++----- .../MFATests/EmailMFATests/EmailMFAOnlyTests.swift | 10 +++++----- .../EmailMFAWithAllMFATypesRequiredTests.swift | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Amplify/Categories/Auth/Models/AuthSignInStep.swift b/Amplify/Categories/Auth/Models/AuthSignInStep.swift index ba02152d61..26837b627b 100644 --- a/Amplify/Categories/Auth/Models/AuthSignInStep.swift +++ b/Amplify/Categories/Auth/Models/AuthSignInStep.swift @@ -43,15 +43,15 @@ public enum AuthSignInStep { /// case continueSignInWithEmailMFASetup - /// Auth step is EMAIL multi factor authentication. - /// - /// Confirmation code for the MFA will be send to the provided EMAIL. - case confirmSignInWithEmailMFACode(AuthCodeDeliveryDetails) - /// Auth step is for continuing sign in by selecting multi factor authentication type to setup /// case continueSignInWithMFASetupSelection(AllowedMFATypes) + /// Auth step is for confirming sign in with OTP + /// + /// OTP for the factor will be sent to the delivery medium. + case confirmSignInWithOTP(AuthCodeDeliveryDetails) + /// Auth step required the user to change their password. /// case resetPassword(AdditionalInfo?) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift index 3d37525eb8..f631e133d3 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/InitializeResolveChallenge.swift @@ -51,7 +51,7 @@ struct InitializeResolveChallenge: Action { case .selectMFAType: return .continueSignInWithMFASelection(challenge.getAllowedMFATypesForSelection) case .emailMFA: - return .confirmSignInWithEmailMFACode(challenge.codeDeliveryDetails) + return .confirmSignInWithOTP(challenge.codeDeliveryDetails) case .setUpMFA: var allowedMFATypesForSetup = challenge.getAllowedMFATypesForSetup // remove SMS, as it is not supported and should not be sent back to the customer, since it could be misleading diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift index 7be4e8960e..92e34f6898 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/EmailMFATests.swift @@ -97,14 +97,14 @@ class EmailMFATests: BasePluginTest { } } - /// Test a signIn with valid inputs getting confirmSignInWithEmailMFACode challenge + /// Test a signIn with valid inputs getting confirmSignInWithOTP challenge /// /// - Given: Given an auth plugin with mocked service. /// /// - When: /// - I invoke signIn with valid values /// - Then: - /// - I should get a .confirmSignInWithEmailMFACode response + /// - I should get a .confirmSignInWithOTP response /// func testSuccessfulEmailMFACodeStep() async { var signInStepIterator = 0 @@ -134,8 +134,8 @@ class EmailMFATests: BasePluginTest { username: "username", password: "password", options: AuthSignInRequest.Options()) - guard case .confirmSignInWithEmailMFACode(let codeDetails) = result.nextStep else { - XCTFail("Result should be .confirmSignInWithEmailMFACode for next step, instead got: \(result.nextStep)") + guard case .confirmSignInWithOTP(let codeDetails) = result.nextStep else { + XCTFail("Result should be .confirmSignInWithOTP for next step, instead got: \(result.nextStep)") return } if case .email(let destination) = codeDetails.destination { @@ -232,7 +232,7 @@ class EmailMFATests: BasePluginTest { signInStepIterator = 1 confirmSignInResult = try await plugin.confirmSignIn( challengeResponse: "test@test.com") - guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { + guard case .confirmSignInWithOTP(let deliveryDetails) = confirmSignInResult.nextStep else { XCTFail("Result should be .continueSignInWithEmailMFASetup but got: \(confirmSignInResult.nextStep)") return } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift index bac8447467..6f1be2aa0e 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAOnlyTests.swift @@ -89,8 +89,8 @@ class EmailMFARequiredTests: AWSAuthBaseTest { challengeResponse: username + "@integTest.com") // Step 6: Ensure that the next step is to confirm the Email MFA code - guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { - XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(confirmSignInResult.nextStep)") + guard case .confirmSignInWithOTP(let deliveryDetails) = confirmSignInResult.nextStep else { + XCTFail("Expected .confirmSignInWithOTP step, got \(confirmSignInResult.nextStep)") return } if case .email(let destination) = deliveryDetails.destination { @@ -137,7 +137,7 @@ class EmailMFARequiredTests: AWSAuthBaseTest { /// they should successfully complete the MFA process and sign in. /// /// - MFA Setup Flow: - /// - Step 1: User signs in and receives the `confirmSignInWithEmailMFACode` challenge. + /// - Step 1: User signs in and receives the `confirmSignInWithOTP` challenge. /// - Step 2: User enters an incorrect MFA code and receives a `codeMismatch` error. /// - Step 3: User enters the correct MFA code. /// - Step 4: Sign-in completes, and the email is associated with the user account. @@ -165,8 +165,8 @@ class EmailMFARequiredTests: AWSAuthBaseTest { options: options) // Step 6: Ensure that the next step is to confirm the Email MFA code - guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = result.nextStep else { - XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(result.nextStep)") + guard case .confirmSignInWithOTP(let deliveryDetails) = result.nextStep else { + XCTFail("Expected .confirmSignInWithOTP step, got \(result.nextStep)") return } if case .email(let destination) = deliveryDetails.destination { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift index 8b6dc32616..88dd39ec57 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/EmailMFATests/EmailMFAWithAllMFATypesRequiredTests.swift @@ -112,8 +112,8 @@ class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { options: AuthSignInRequest.Options()) // Step 4: Verify that the next step is to confirm the Email MFA code - guard case .confirmSignInWithEmailMFACode(let codeDetails) = result.nextStep else { - XCTFail("Expected .confirmSignInWithEmailMFACode step, got \(result.nextStep)") + guard case .confirmSignInWithOTP(let codeDetails) = result.nextStep else { + XCTFail("Expected .confirmSignInWithOTP step, got \(result.nextStep)") return } if case .email(let destination) = codeDetails.destination { @@ -193,8 +193,8 @@ class EmailMFAWithAllMFATypesRequiredTests: AWSAuthBaseTest { challengeResponse: username + "@integTest.com") // Step 8: Verify that the next step is to confirm the Email MFA code - guard case .confirmSignInWithEmailMFACode(let deliveryDetails) = confirmSignInResult.nextStep else { - XCTFail("Expected .confirmSignInWithEmailMFACode step") + guard case .confirmSignInWithOTP(let deliveryDetails) = confirmSignInResult.nextStep else { + XCTFail("Expected .confirmSignInWithOTP step") return } if case .email(let destination) = deliveryDetails.destination {