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 1/3] 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 2/3] 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 3/3] 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)