diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift index 52d1c3441d..a239bbeba4 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift @@ -225,7 +225,7 @@ extension AWSCognitoAuthPlugin: AuthCategoryBehavior { let request = AuthConfirmSignInWithMagicLinkRequest( challengeResponse: challengeResponse, options: options) - let task = AWSAuthConfirmSignInWithMagicLinkTask( + let task = try AWSAuthConfirmSignInWithMagicLinkTask( request, stateMachine: authStateMachine, configuration: authConfiguration) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/PasswordlessCustomAuthRequest.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/PasswordlessCustomAuthRequest.swift index e0fd1bf115..af18c68fba 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/PasswordlessCustomAuthRequest.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/PasswordlessCustomAuthRequest.swift @@ -20,7 +20,7 @@ enum PasswordlessCustomAuthRequestAction: String { struct PasswordlessCustomAuthRequest { - private let namespace = "amplify.passwordless" + private let namespace = "Amplify.Passwordless" let signInMethod: PasswordlessCustomAuthSignInMethod let action: PasswordlessCustomAuthRequestAction diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessConfirmSignInHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessConfirmSignInHelper.swift new file mode 100644 index 0000000000..0246b97f65 --- /dev/null +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessConfirmSignInHelper.swift @@ -0,0 +1,117 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify + +struct PasswordlessConfirmSignInHelper: DefaultLogger { + + private let authStateMachine: AuthStateMachine + private let taskHelper: AWSAuthTaskHelper + private let challengeResponse: String + private let confirmSignInRequestMetadata: PasswordlessCustomAuthRequest + private let pluginOptions: Any? + + init(authStateMachine: AuthStateMachine, + challengeResponse: String, + confirmSignInRequestMetadata: PasswordlessCustomAuthRequest, + pluginOptions: Any?) { + + self.authStateMachine = authStateMachine + self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) + self.challengeResponse = challengeResponse + self.confirmSignInRequestMetadata = confirmSignInRequestMetadata + self.pluginOptions = pluginOptions + } + + func confirmSignIn() async throws -> AuthSignInResult { + log.verbose("Starting execution") + await taskHelper.didStateMachineConfigured() + + let invalidStateError = AuthError.invalidState( + "User is not attempting signIn operation", + AuthPluginErrorConstants.invalidStateError, nil) + + guard case .configured(let authNState, _) = await authStateMachine.currentState, + case .signingIn(let signInState) = authNState else { + throw invalidStateError + } + + guard case .resolvingChallenge(let challengeState, _, _) = signInState else { + throw invalidStateError + } + + switch challengeState { + case .waitingForAnswer, .error: + log.verbose("Sending confirm signIn event: \(challengeState)") + await sendConfirmSignInEvent() + default: + throw invalidStateError + } + + let stateSequences = await authStateMachine.listen() + log.verbose("Waiting for response") + for await state in stateSequences { + guard case .configured(let authNState, let authZState) = state else { + continue + } + switch authNState { + case .signedIn: + if case .sessionEstablished = authZState { + return AuthSignInResult(nextStep: .done) + } else { + log.verbose("Signed In, waiting for authorization to complete") + } + case .error(let error): + throw AuthError.unknown("Sign in reached an error state", error) + + case .signingIn(let signInState): + guard let result = try UserPoolSignInHelper.checkNextStep(signInState) else { + continue + } + return result + case .notConfigured: + throw AuthError.configuration( + "UserPool configuration is missing", + AuthPluginErrorConstants.configurationError) + default: + throw invalidStateError + } + } + throw invalidStateError + } + + private func sendConfirmSignInEvent() async { + let event = SignInChallengeEvent( + eventType: .verifyChallengeAnswer(createConfirmSignInEventData())) + await authStateMachine.send(event) + } + + private func createConfirmSignInEventData() -> ConfirmSignInEventData { + var passwordlessMetadata = confirmSignInRequestMetadata.toDictionary() + if let customerMetadata = (pluginOptions as? AWSAuthConfirmSignInWithOTPOptions)?.metadata { + passwordlessMetadata.merge(customerMetadata, uniquingKeysWith: { passwordlessMetadata, customerMetadata in + // Ideally key collision won't happen, because passwordless has been namespaced + // if for some reason collision still happens, + // prioritizing passwordlessFlow keys for flow to continue without any issues. + passwordlessMetadata + + }) + } else if let customerMetadata = (pluginOptions as? AWSAuthConfirmSignInWithMagicLinkOptions)?.metadata { + passwordlessMetadata.merge(customerMetadata, uniquingKeysWith: { passwordlessMetadata, customerMetadata in + // Ideally key collision won't happen, because passwordless has been namespaced + // if for some reason collision still happens, + // prioritizing passwordlessFlow keys for flow to continue without any issues. + passwordlessMetadata + + }) + } + return ConfirmSignInEventData( + answer: challengeResponse, + metadata: passwordlessMetadata) + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessSignInHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessSignInHelper.swift new file mode 100644 index 0000000000..6f1a3ef792 --- /dev/null +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/PasswordlessSignInHelper.swift @@ -0,0 +1,292 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify + +struct PasswordlessSignInHelper: DefaultLogger { + + private let authStateMachine: AuthStateMachine + private let taskHelper: AWSAuthTaskHelper + private let authConfiguration: AuthConfiguration? + private let username: String + private let challengeAnswer: String + private let signInRequestMetadata: PasswordlessCustomAuthRequest + private let passwordlessFlow: AuthPasswordlessFlow + private let pluginOptions: Any? + + // TODO: Add authEnvironment parameter here to access URLSessionClient + init(authStateMachine: AuthStateMachine, + configuration: AuthConfiguration?, + username: String, + challengeAnswer: String, + signInRequestMetadata: PasswordlessCustomAuthRequest, + passwordlessFlow: AuthPasswordlessFlow, + pluginOptions: Any?) { + + self.authStateMachine = authStateMachine + self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) + self.authConfiguration = configuration + self.username = username + self.challengeAnswer = challengeAnswer + self.signInRequestMetadata = signInRequestMetadata + self.passwordlessFlow = passwordlessFlow + self.pluginOptions = pluginOptions + } + + func signIn() async throws -> AuthSignInResult { + + log.verbose("Starting execution") + await taskHelper.didStateMachineConfigured() + + do { + // Make sure current state is a valid state to initialize sign in + try await validateCurrentState() + + if passwordlessFlow == .signUpAndSignIn { + // Check if we have a user pool configuration + // User pool configuration is used retrieve API Gateway information, + // so that sign up flow can take place + guard let userPoolConfiguration = authConfiguration?.getUserPoolConfiguration() else { + let message = AuthPluginErrorConstants.configurationError + let authError = AuthError.configuration( + "Could not find user pool configuration", + message) + throw authError + } + + log.verbose("Starting Passwordless Sign Up flow") + // [HS] TODO: Proceed to sign up flow first + } + + // Start sign in + return try await startPasswordlessSignIn() + } catch { + + log.error(error: error) + // If Passwordless Sign in failed, send sign in cancellation event + await sendCancelSignInEvent() + + // Wait for sign in cancellation to complete + await waitForSignInCancel() + + // throw error that came during sign in + throw error + } + } + + + + private func startPasswordlessSignIn() async throws -> AuthSignInResult { + + log.verbose("Starting Passwordless Sign In flow") + let result = try await doSignIn() + + log.verbose("Received result") + return result + } + + private func doSignIn() async throws -> AuthSignInResult { + + log.verbose("Sending initiate Sign In event") + await sendInitiateSignInEvent() + + log.verbose("Start listening to state machine changes") + return try await listenToStateChanges() + } + + private func listenToStateChanges() async throws -> AuthSignInResult { + let stateSequences = await authStateMachine.listen() + for await state in stateSequences { + guard case .configured(let authNState, let authZState) = state else { + continue + } + switch authNState { + case .signedIn: + if case .sessionEstablished = authZState { + return AuthSignInResult(nextStep: .done) + } else if case .error(let error) = authZState { + log.verbose("Authorization reached an error state \(error)") + throw error.authError + } + case .error(let error): + throw error.authError + case .signingIn(let signInState): + guard let nextStepResult = try UserPoolSignInHelper.checkNextStep(signInState) else { + continue + } + guard let signInResult = try await parseAndValidate(signInResult: nextStepResult) else { + continue + } + return signInResult + default: + continue + } + } + throw AuthError.unknown("Sign in reached an error state") + } + + private func parseAndValidate(signInResult: AuthSignInResult) async throws -> AuthSignInResult? { + + guard case .confirmSignInWithCustomChallenge(let challengeParams) = signInResult.nextStep else { + log.error("Did not receive custom auth challenge as a next Step instead received: \(signInResult)") + throw AuthError.service( + "Did not receive custom auth challenge as a next Step.", + AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) + } + guard let nextStepString = challengeParams?["nextStep"] else { + log.error("Did not receive a valid next step. Received Challenge Params: \(challengeParams ?? [:])") + throw AuthError.service( + "Did not receive a valid next step for Passwordless \(signInRequestMetadata.signInMethod) flow.", + AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) + } + + guard let nextStep = PasswordlessCustomAuthNextStep(rawValue: nextStepString) else { + log.error("Invalid next step. Next Step\(nextStepString)") + throw AuthError.service( + "Did not receive a valid next step for Passwordless \(signInRequestMetadata.signInMethod) flow.", + AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) + } + + switch nextStep { + case .provideAuthParameters: + log.verbose("Sending Response for InitAuth challenge") + // Library handles creating auth parameters + await sendEventForProvidingAuthParameters() + return nil + + case .provideChallengeResponse: + // Ask the customer for the next step + switch signInRequestMetadata.signInMethod{ + case .otp: + return .init(nextStep: .confirmSignInWithOTP( + getCodeDeliveryDetails(parameters: challengeParams ?? [:]), nil)) + case .magicLink: + return .init(nextStep: .confirmSignInWithMagicLink( + getCodeDeliveryDetails(parameters: challengeParams ?? [:]), nil)) + } + } + } + + // MARK: Events + + private func sendInitiateSignInEvent() async { + let signInData = SignInEventData( + username: username, + clientMetadata: customerClientMetadata(), + signInMethod: .apiBased(.customWithoutSRP) + ) + let event = AuthenticationEvent.init(eventType: .signInRequested(signInData)) + await authStateMachine.send(event) + } + + private func sendEventForProvidingAuthParameters() async { + var passwordlessFlowMetadata = signInRequestMetadata.toDictionary() + passwordlessFlowMetadata.merge(customerClientMetadata()) { passwordlessFlowMetadata, customerClientMetadata in + // Ideally key collision won't happen, because passwordless has been namespaced + // if for some reason collision still happens, + // prioritizing passwordlessFlow keys for flow to continue without any issues. + passwordlessFlowMetadata + } + let confirmSignInEventData = ConfirmSignInEventData( + answer: challengeAnswer, + metadata: passwordlessFlowMetadata) + let event = SignInChallengeEvent( + eventType: .verifyChallengeAnswer(confirmSignInEventData)) + await authStateMachine.send(event) + } + + // MARK: State Validations + + private func validateCurrentState() async throws { + + let stateSequences = await authStateMachine.listen() + log.verbose("Validating current state") + for await state in stateSequences { + guard case .configured(let authenticationState, _) = state else { + continue + } + switch authenticationState { + case .signedIn: + let error = AuthError.invalidState( + "There is already a user in signedIn state. SignOut the user first before calling signIn", + AuthPluginErrorConstants.invalidStateError, nil) + throw error + case .signingIn: + log.verbose("Cancelling existing signIn flow") + await sendCancelSignInEvent() + case .signedOut: + return + default: continue + } + } + } + + // MARK: Private helpers + + private func customerClientMetadata() -> [String: String] { + + if let options = pluginOptions as? AWSAuthSignUpAndSignInPasswordlessOptions, + let clientMetadata = options.clientMetadata { + return clientMetadata + } else if let options = pluginOptions as? AWSAuthSignInPasswordlessOptions, + let clientMetadata = options.clientMetadata { + return clientMetadata + } else if let options = pluginOptions as? AWSAuthConfirmSignInWithMagicLinkOptions, + let clientMetadata = options.metadata { + return clientMetadata + } + return [:] + } + + private func getCodeDeliveryDetails(parameters: [String: String]) -> AuthCodeDeliveryDetails { + + var deliveryDestination = DeliveryDestination.unknown(nil) + var attribute: AuthUserAttributeKey? = nil + + // Retrieve Delivery medium and destination + let medium = parameters["deliveryMedium"] + let destination = parameters["destination"] + if medium == "SMS" { + deliveryDestination = .sms(destination) + } else if medium == "EMAIL" { + deliveryDestination = .email(destination) + } + + // Retrieve attribute name + if let attributeName = parameters["attributeName"] { + attribute = AuthUserAttributeKey(rawValue: attributeName) + } + + return AuthCodeDeliveryDetails( + destination: deliveryDestination, + attributeKey: attribute) + } + + // MARK: Sign In Cancellation + + private func sendCancelSignInEvent() async { + let event = AuthenticationEvent(eventType: .cancelSignIn) + await authStateMachine.send(event) + } + + private func waitForSignInCancel() async { + let stateSequences = await authStateMachine.listen() + log.verbose("Wait for signIn to cancel") + for await state in stateSequences { + guard case .configured(let authenticationState, _) = state else { + continue + } + switch authenticationState { + case .signedOut: + return + default: continue + } + } + } + +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/MagicLinkTokenParser.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/MagicLinkTokenParser.swift new file mode 100644 index 0000000000..018453d409 --- /dev/null +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/MagicLinkTokenParser.swift @@ -0,0 +1,40 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct MagicLinkTokenParser { + + static func extractUserName(from token: String) throws -> String { + let tokenSplit = token.split(separator: ".") + guard tokenSplit.count == 2 else { + throw SignInError.invalidServiceResponse( + message: "Malformed magic link token") + } + let base64 = tokenSplit[0] + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + let paddedLength = base64.count + (4 - (base64.count % 4)) % 4 + + let base64Padding = base64.padding(toLength: paddedLength, withPad: "=", startingAt: 0) + guard let encodedData = Data(base64Encoded: base64Padding, + options: .ignoreUnknownCharacters), + let jsonObject = try? JSONSerialization.jsonObject( + with: encodedData, + options: []) as? [String: Any] else { + throw SignInError.invalidServiceResponse( + message: "Unable to to decode magic link token") + } + + guard let username = jsonObject["username"] as? String else { + throw SignInError.invalidServiceResponse( + message: "Did not find username object in magic link token") + } + + return username + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithMagicLinkTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithMagicLinkTask.swift index 3b8a5bb868..ed63332e57 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithMagicLinkTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithMagicLinkTask.swift @@ -10,11 +10,7 @@ import AWSPluginsCore class AWSAuthConfirmSignInWithMagicLinkTask: AuthConfirmSignInWithMagicLinkTask, DefaultLogger { - private let request: AuthConfirmSignInWithMagicLinkRequest - private let authStateMachine: AuthStateMachine - private let taskHelper: AWSAuthTaskHelper - private let authConfiguration: AuthConfiguration - private let confirmSignInRequestMetadata: PasswordlessCustomAuthRequest + let passwordlessSignInHelper: PasswordlessSignInHelper var eventName: HubPayloadEventName { HubPayload.EventName.Auth.confirmSignInWithMagicLinkAPI @@ -22,93 +18,23 @@ class AWSAuthConfirmSignInWithMagicLinkTask: AuthConfirmSignInWithMagicLinkTask, init(_ request: AuthConfirmSignInWithMagicLinkRequest, stateMachine: AuthStateMachine, - configuration: AuthConfiguration) { - self.request = request - self.authStateMachine = stateMachine - self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) - self.authConfiguration = configuration - self.confirmSignInRequestMetadata = .init(signInMethod: .magicLink, action: .confirm) + configuration: AuthConfiguration) throws { + + let username = try MagicLinkTokenParser.extractUserName(from: request.challengeResponse) + passwordlessSignInHelper = PasswordlessSignInHelper( + authStateMachine: stateMachine, + configuration: nil, + username: username, + challengeAnswer: request.challengeResponse, + signInRequestMetadata: .init( + signInMethod: .magicLink, + action: .confirm), + passwordlessFlow: .signIn, + pluginOptions: request.options.pluginOptions) } func execute() async throws -> AuthSignInResult { - log.verbose("Starting execution") - await taskHelper.didStateMachineConfigured() - - if let validationError = request.hasError() { - throw validationError - } - let invalidStateError = AuthError.invalidState( - "User is not attempting signIn operation", - AuthPluginErrorConstants.invalidStateError, nil) - - guard case .configured(let authNState, _) = await authStateMachine.currentState, - case .signingIn(let signInState) = authNState else { - throw invalidStateError - } - - guard case .resolvingChallenge(let challengeState, _, _) = signInState else { - throw invalidStateError - } - - switch challengeState { - case .waitingForAnswer, .error: - log.verbose("Sending confirm signIn event: \(challengeState)") - await sendConfirmSignInEvent() - default: - throw invalidStateError - } - - let stateSequences = await authStateMachine.listen() - log.verbose("Waiting for response") - for await state in stateSequences { - guard case .configured(let authNState, let authZState) = state else { - continue - } - switch authNState { - case .signedIn: - if case .sessionEstablished = authZState { - return AuthSignInResult(nextStep: .done) - } else { - log.verbose("Signed In, waiting for authorization to complete") - } - case .error(let error): - throw AuthError.unknown("Sign in reached an error state", error) - - case .signingIn(let signInState): - guard let result = try UserPoolSignInHelper.checkNextStep(signInState) else { - continue - } - return result - case .notConfigured: - throw AuthError.configuration( - "UserPool configuration is missing", - AuthPluginErrorConstants.configurationError) - default: - throw invalidStateError - } - } - throw invalidStateError - } - - func sendConfirmSignInEvent() async { - let event = SignInChallengeEvent( - eventType: .verifyChallengeAnswer(createConfirmSignInEventData())) - await authStateMachine.send(event) + return try await passwordlessSignInHelper.signIn() } - private func createConfirmSignInEventData() -> ConfirmSignInEventData { - var passwordlessMetadata = confirmSignInRequestMetadata.toDictionary() - if let customerMetadata = (request.options.pluginOptions as? AWSAuthConfirmSignInWithMagicLinkOptions)?.metadata { - passwordlessMetadata.merge(customerMetadata, uniquingKeysWith: { passwordlessMetadata, customerMetadata in - // Ideally key collision won't happen, because passwordless has been namespaced - // if for some reason collision still happens, - // prioritizing passwordlessFlow keys for flow to continue without any issues. - passwordlessMetadata - - }) - } - return ConfirmSignInEventData( - answer: self.request.challengeResponse, - metadata: passwordlessMetadata) - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithOTPTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithOTPTask.swift index ca425c6e85..b111824260 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithOTPTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInWithOTPTask.swift @@ -10,11 +10,8 @@ import AWSPluginsCore class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogger { + private let confirmSignInHelper: PasswordlessConfirmSignInHelper private let request: AuthConfirmSignInWithOTPRequest - private let authStateMachine: AuthStateMachine - private let taskHelper: AWSAuthTaskHelper - private let authConfiguration: AuthConfiguration - private let confirmSignInRequestMetadata: PasswordlessCustomAuthRequest var eventName: HubPayloadEventName { HubPayload.EventName.Auth.confirmSignInWithOTPAPI @@ -24,91 +21,17 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg stateMachine: AuthStateMachine, configuration: AuthConfiguration) { self.request = request - self.authStateMachine = stateMachine - self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) - self.authConfiguration = configuration - self.confirmSignInRequestMetadata = .init(signInMethod: .otp, action: .confirm) + self.confirmSignInHelper = PasswordlessConfirmSignInHelper( + authStateMachine: stateMachine, + challengeResponse: request.challengeResponse, + confirmSignInRequestMetadata: .init(signInMethod: .otp, action: .confirm), + pluginOptions: request.options.pluginOptions) } func execute() async throws -> AuthSignInResult { - log.verbose("Starting execution") - await taskHelper.didStateMachineConfigured() - if let validationError = request.hasError() { throw validationError } - let invalidStateError = AuthError.invalidState( - "User is not attempting signIn operation", - AuthPluginErrorConstants.invalidStateError, nil) - - guard case .configured(let authNState, _) = await authStateMachine.currentState, - case .signingIn(let signInState) = authNState else { - throw invalidStateError - } - - guard case .resolvingChallenge(let challengeState, _, _) = signInState else { - throw invalidStateError - } - - switch challengeState { - case .waitingForAnswer, .error: - log.verbose("Sending confirm signIn event: \(challengeState)") - await sendConfirmSignInEvent() - default: - throw invalidStateError - } - - let stateSequences = await authStateMachine.listen() - log.verbose("Waiting for response") - for await state in stateSequences { - guard case .configured(let authNState, let authZState) = state else { - continue - } - switch authNState { - case .signedIn: - if case .sessionEstablished = authZState { - return AuthSignInResult(nextStep: .done) - } else { - log.verbose("Signed In, waiting for authorization to complete") - } - case .error(let error): - throw AuthError.unknown("Sign in reached an error state", error) - - case .signingIn(let signInState): - guard let result = try UserPoolSignInHelper.checkNextStep(signInState) else { - continue - } - return result - case .notConfigured: - throw AuthError.configuration( - "UserPool configuration is missing", - AuthPluginErrorConstants.configurationError) - default: - throw invalidStateError - } - } - throw invalidStateError - } - - func sendConfirmSignInEvent() async { - let event = SignInChallengeEvent( - eventType: .verifyChallengeAnswer(createConfirmSignInEventData())) - await authStateMachine.send(event) - } - - private func createConfirmSignInEventData() -> ConfirmSignInEventData { - var passwordlessMetadata = confirmSignInRequestMetadata.toDictionary() - if let customerMetadata = (request.options.pluginOptions as? AWSAuthConfirmSignInWithOTPOptions)?.metadata { - passwordlessMetadata.merge(customerMetadata, uniquingKeysWith: { passwordlessMetadata, customerMetadata in - // Ideally key collision won't happen, because passwordless has been namespaced - // if for some reason collision still happens, - // prioritizing passwordlessFlow keys for flow to continue without any issues. - passwordlessMetadata - - }) - } - return ConfirmSignInEventData( - answer: self.request.challengeResponse, - metadata: passwordlessMetadata) + return try await confirmSignInHelper.confirmSignIn() } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithMagicLinkTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithMagicLinkTask.swift index ade7128912..4c9994ea84 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithMagicLinkTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithMagicLinkTask.swift @@ -10,11 +10,7 @@ import AWSPluginsCore class AWSAuthSignInWithMagicLinkTask: AuthSignInWithMagicLinkTask, DefaultLogger { - private let request: AuthSignInWithMagicLinkRequest - private let authStateMachine: AuthStateMachine - private let taskHelper: AWSAuthTaskHelper - private let authConfiguration: AuthConfiguration - private let signInMetadataRequestMetadata: PasswordlessCustomAuthRequest + let passwordlessSignInHelper: PasswordlessSignInHelper var eventName: HubPayloadEventName { HubPayload.EventName.Auth.signInWithMagicLinkAPI @@ -24,255 +20,23 @@ class AWSAuthSignInWithMagicLinkTask: AuthSignInWithMagicLinkTask, DefaultLogger init(_ request: AuthSignInWithMagicLinkRequest, authStateMachine: AuthStateMachine, configuration: AuthConfiguration) { - self.request = request - self.authStateMachine = authStateMachine - self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) - self.authConfiguration = configuration - self.signInMetadataRequestMetadata = .init( - signInMethod: .magicLink, action: .request, deliveryMedium: .email, redirectURL: request.redirectURL) - } - - func execute() async throws -> AuthSignInResult { - - log.verbose("Starting execution") - await taskHelper.didStateMachineConfigured() - - // Check if we have a user pool configuration - // User pool configuration is used retrieve API Gateway information, - // so that sign up flow can take place - guard let userPoolConfiguration = authConfiguration.getUserPoolConfiguration() else { - let message = AuthPluginErrorConstants.configurationError - let authError = AuthError.configuration( - "Could not find user pool configuration", - message) - throw authError - } - - // Make sure current state is a valid state to initialize sign in - try await validateCurrentState() - - do { - // Start magic link sign in - return try await startMagicLinkSignIn() - } catch { - - log.error(error: error) - // If Magic Link Sign in failed, send sign in cancellation event - await sendCancelSignInEvent() - - // Wait for sign in cancellation to complete - await waitForSignInCancel() - - // throw error that came during sign in - throw error - } - } - - - - private func startMagicLinkSignIn() async throws -> AuthSignInResult { - if request.flow == .signUpAndSignIn { - log.verbose("Starting Magic Link Passwordless Sign Up flow") - // TODO: Access the URLSession Client and auth configuration and make HTTP - // POST to API Gateway endpoint - } - - log.verbose("Starting Magic Link Passwordless Sign In flow") - let result = try await doSignIn() - - log.verbose("Received result") - return result - } - - private func doSignIn() async throws -> AuthSignInResult { - - log.verbose("Sending initiate Sign In event") - await sendInitiateSignInEvent() - - log.verbose("Start listening to state machine changes") - return try await listenToStateChanges() - } - - func listenToStateChanges() async throws -> AuthSignInResult { - let stateSequences = await authStateMachine.listen() - for await state in stateSequences { - guard case .configured(let authNState, let authZState) = state else { - continue - } - switch authNState { - case .signedIn: - if case .sessionEstablished = authZState { - return AuthSignInResult(nextStep: .done) - } else if case .error(let error) = authZState { - log.verbose("Authorization reached an error state \(error)") - throw error.authError - } - case .error(let error): - throw error.authError - case .signingIn(let signInState): - guard let nextStepResult = try UserPoolSignInHelper.checkNextStep(signInState) else { - continue - } - guard let signInResult = try await parseAndValidate(signInResult: nextStepResult) else { - continue - } - return signInResult - default: - continue - } - } - throw AuthError.unknown("Sign in reached an error state") - } - - func parseAndValidate(signInResult: AuthSignInResult) async throws -> AuthSignInResult? { - - guard case .confirmSignInWithCustomChallenge(let challengeParams) = signInResult.nextStep else { - log.error("Did not receive custom auth challenge as a next Step instead received: \(signInResult)") - throw AuthError.service( - "Did not receive custom auth challenge as a next Step.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - guard let nextStepString = challengeParams?["nextStep"] else { - log.error("Did not receive a valid next step. Received Challenge Params: \(challengeParams ?? [:])") - throw AuthError.service( - "Did not receive a valid next step for Passwordless Magic Link flow.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - - guard let nextStep = PasswordlessCustomAuthNextStep(rawValue: nextStepString) else { - log.error("Invalid next step. Next Step\(nextStepString)") - throw AuthError.service( - "Did not receive a valid next step for Passwordless Magic Link flow.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - - switch nextStep { - case .provideAuthParameters: - log.verbose("Sending Response for InitAuth challenge") - // Library handles creating auth parameters - await sendEventForProvidingAuthParameters() - return nil - - case .provideChallengeResponse: - // Ask the customer for magiclink code - return .init(nextStep: .confirmSignInWithMagicLink( - getCodeDeliveryDetails(parameters: challengeParams ?? [:]), nil)) - } - } - - // MARK: Events - - private func sendInitiateSignInEvent() async { - let signInData = SignInEventData( + passwordlessSignInHelper = PasswordlessSignInHelper( + authStateMachine: authStateMachine, + configuration: configuration, username: request.username, - clientMetadata: customerClientMetadata(), - signInMethod: .apiBased(.customWithoutSRP) - ) - let event = AuthenticationEvent.init(eventType: .signInRequested(signInData)) - await authStateMachine.send(event) - } - - private func sendEventForProvidingAuthParameters() async { - var passwordlessFlowMetadata = signInMetadataRequestMetadata.toDictionary() - passwordlessFlowMetadata.merge(customerClientMetadata()) { passwordlessFlowMetadata, customerClientMetadata in - // Ideally key collision won't happen, because passwordless has been namespaced - // if for some reason collision still happens, - // prioritizing passwordlessFlow keys for flow to continue without any issues. - passwordlessFlowMetadata - } - let confirmSignInEventData = ConfirmSignInEventData( // NOTE: answer is not applicable in this scenario - // because this event is only responsible for initializing the passwordless magiclink workflow - answer: "", - metadata: passwordlessFlowMetadata) - let event = SignInChallengeEvent( - eventType: .verifyChallengeAnswer(confirmSignInEventData)) - await authStateMachine.send(event) + // because this event is only responsible for initializing the passwordless OTP workflow + challengeAnswer: "", + signInRequestMetadata: .init( + signInMethod: .magicLink, + action: .request, + deliveryMedium: .email, + redirectURL: request.redirectURL), + passwordlessFlow: request.flow, + pluginOptions: request.options.pluginOptions) } - // MARK: State Validations - - private func validateCurrentState() async throws { - - let stateSequences = await authStateMachine.listen() - log.verbose("Validating current state") - for await state in stateSequences { - guard case .configured(let authenticationState, _) = state else { - continue - } - switch authenticationState { - case .signedIn: - let error = AuthError.invalidState( - "There is already a user in signedIn state. SignOut the user first before calling signIn", - AuthPluginErrorConstants.invalidStateError, nil) - throw error - case .signingIn: - log.verbose("Cancelling existing signIn flow") - await sendCancelSignInEvent() - case .signedOut: - return - default: continue - } - } - } - - // MARK: Private helpers - - private func customerClientMetadata() -> [String: String] { - - if let options = request.options.pluginOptions as? AWSAuthSignUpAndSignInPasswordlessOptions, - let clientMetadata = options.clientMetadata { - return clientMetadata - } else if let options = request.options.pluginOptions as? AWSAuthSignInPasswordlessOptions, - let clientMetadata = options.clientMetadata { - return clientMetadata - } - return [:] - } - - private func getCodeDeliveryDetails(parameters: [String: String]) -> AuthCodeDeliveryDetails { - - var deliveryDestination = DeliveryDestination.unknown(nil) - var attribute: AuthUserAttributeKey? = nil - - // Retrieve Delivery medium and destination - let medium = parameters["deliveryMedium"] - let destination = parameters["destination"] - if medium == "SMS" { - deliveryDestination = .sms(destination) - } else if medium == "EMAIL" { - deliveryDestination = .email(destination) - } - - // Retrieve attribute name - if let attributeName = parameters["attributeName"] { - attribute = AuthUserAttributeKey(rawValue: attributeName) - } - - return AuthCodeDeliveryDetails( - destination: deliveryDestination, - attributeKey: attribute) - } - - // MARK: Sign In Cancellation - - private func sendCancelSignInEvent() async { - let event = AuthenticationEvent(eventType: .cancelSignIn) - await authStateMachine.send(event) - } - - private func waitForSignInCancel() async { - let stateSequences = await authStateMachine.listen() - log.verbose("Wait for signIn to cancel") - for await state in stateSequences { - guard case .configured(let authenticationState, _) = state else { - continue - } - switch authenticationState { - case .signedOut: - return - default: continue - } - } + func execute() async throws -> AuthSignInResult { + return try await passwordlessSignInHelper.signIn() } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithOTPTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithOTPTask.swift index 7f61d4a690..1f1ed2f159 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithOTPTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInWithOTPTask.swift @@ -10,267 +10,32 @@ import AWSPluginsCore class AWSAuthSignInWithOTPTask: AuthSignInWithOTPTask, DefaultLogger { - private let request: AuthSignInWithOTPRequest - private let authStateMachine: AuthStateMachine - private let taskHelper: AWSAuthTaskHelper - private let authConfiguration: AuthConfiguration - private let signInMetadataRequestMetadata: PasswordlessCustomAuthRequest + let passwordlessSignInHelper: PasswordlessSignInHelper var eventName: HubPayloadEventName { HubPayload.EventName.Auth.signInWithOTPAPI } + // TODO: Add authEnvironment parameter here to access URLSessionClient init(_ request: AuthSignInWithOTPRequest, authStateMachine: AuthStateMachine, configuration: AuthConfiguration) { - self.request = request - self.authStateMachine = authStateMachine - self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine) - self.authConfiguration = configuration - self.signInMetadataRequestMetadata = .init( - signInMethod: .otp, action: .request, deliveryMedium: request.destination) - } - - func execute() async throws -> AuthSignInResult { - - log.verbose("Starting execution") - await taskHelper.didStateMachineConfigured() - - // Check if we have a user pool configuration - // User pool configuration is used retrieve API Gateway information, - // so that sign up flow can take place - guard let userPoolConfiguration = authConfiguration.getUserPoolConfiguration() else { - let message = AuthPluginErrorConstants.configurationError - let authError = AuthError.configuration( - "Could not find user pool configuration", - message) - throw authError - } - - // Make sure current state is a valid state to initialize sign in - try await validateCurrentState() - - do { - // Start OTP sign in - return try await startOTPSignIn() - } catch { - - log.error(error: error) - // If OTP Sign in failed, send sign in cancellation event - await sendCancelSignInEvent() - - // Wait for sign in cancellation to complete - await waitForSignInCancel() - - // throw error that came during sign in - throw error - } - } - - - - private func startOTPSignIn() async throws -> AuthSignInResult { - if request.flow == .signUpAndSignIn { - log.verbose("Starting OTP Passwordless Sign Up flow") - // [HS] TODO: Proceed to sign up flow first - } - - log.verbose("Starting OTP Passwordless Sign In flow") - let result = try await doSignIn() - - log.verbose("Received result") - return result - } - - private func doSignIn() async throws -> AuthSignInResult { - - log.verbose("Sending initiate Sign In event") - await sendInitiateSignInEvent() - - log.verbose("Start listening to state machine changes") - return try await listenToStateChanges() - } - - func listenToStateChanges() async throws -> AuthSignInResult { - let stateSequences = await authStateMachine.listen() - for await state in stateSequences { - guard case .configured(let authNState, let authZState) = state else { - continue - } - switch authNState { - case .signedIn: - if case .sessionEstablished = authZState { - return AuthSignInResult(nextStep: .done) - } else if case .error(let error) = authZState { - log.verbose("Authorization reached an error state \(error)") - throw error.authError - } - case .error(let error): - throw error.authError - case .signingIn(let signInState): - guard let nextStepResult = try UserPoolSignInHelper.checkNextStep(signInState) else { - continue - } - guard let signInResult = try await parseAndValidate(signInResult: nextStepResult) else { - continue - } - return signInResult - default: - continue - } - } - throw AuthError.unknown("Sign in reached an error state") - } - - func parseAndValidate(signInResult: AuthSignInResult) async throws -> AuthSignInResult? { - - guard case .confirmSignInWithCustomChallenge(let challengeParams) = signInResult.nextStep else { - log.error("Did not receive custom auth challenge as a next Step instead received: \(signInResult)") - throw AuthError.service( - "Did not receive custom auth challenge as a next Step.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - guard let nextStepString = challengeParams?["nextStep"] else { - log.error("Did not receive a valid next step. Received Challenge Params: \(challengeParams ?? [:])") - throw AuthError.service( - "Did not receive a valid next step for Passwordless OTP flow.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - - guard let nextStep = PasswordlessCustomAuthNextStep(rawValue: nextStepString) else { - log.error("Invalid next step. Next Step\(nextStepString)") - throw AuthError.service( - "Did not receive a valid next step for Passwordless OTP flow.", - AmplifyErrorMessages.shouldNotHappenReportBugToAWS(), nil) - } - - switch nextStep { - case .provideAuthParameters: - log.verbose("Sending Response for InitAuth challenge") - // Library handles creating auth parameters - await sendEventForProvidingAuthParameters() - return nil - - case .provideChallengeResponse: - // Ask the customer for OTP - return .init(nextStep: .confirmSignInWithOTP( - getCodeDeliveryDetails(parameters: challengeParams ?? [:]), nil)) - } - } - - // MARK: Events - - private func sendInitiateSignInEvent() async { - let signInData = SignInEventData( + passwordlessSignInHelper = PasswordlessSignInHelper( + authStateMachine: authStateMachine, + configuration: configuration, username: request.username, - clientMetadata: customerClientMetadata(), - signInMethod: .apiBased(.customWithoutSRP) - ) - let event = AuthenticationEvent.init(eventType: .signInRequested(signInData)) - await authStateMachine.send(event) - } - - private func sendEventForProvidingAuthParameters() async { - var passwordlessFlowMetadata = signInMetadataRequestMetadata.toDictionary() - passwordlessFlowMetadata.merge(customerClientMetadata()) { passwordlessFlowMetadata, customerClientMetadata in - // Ideally key collision won't happen, because passwordless has been namespaced - // if for some reason collision still happens, - // prioritizing passwordlessFlow keys for flow to continue without any issues. - passwordlessFlowMetadata - } - let confirmSignInEventData = ConfirmSignInEventData( // NOTE: answer is not applicable in this scenario - // because this event is only responsible for initializing the passwordless OTP workflow - answer: "", - metadata: passwordlessFlowMetadata) - let event = SignInChallengeEvent( - eventType: .verifyChallengeAnswer(confirmSignInEventData)) - await authStateMachine.send(event) + // because this event is only responsible for initializing the passwordless workflow + challengeAnswer: "", + signInRequestMetadata: .init( + signInMethod: .otp, + action: .request, + deliveryMedium: request.destination), + passwordlessFlow: request.flow, + pluginOptions: request.options.pluginOptions) } - // MARK: State Validations - - private func validateCurrentState() async throws { - - let stateSequences = await authStateMachine.listen() - log.verbose("Validating current state") - for await state in stateSequences { - guard case .configured(let authenticationState, _) = state else { - continue - } - switch authenticationState { - case .signedIn: - let error = AuthError.invalidState( - "There is already a user in signedIn state. SignOut the user first before calling signIn", - AuthPluginErrorConstants.invalidStateError, nil) - throw error - case .signingIn: - log.verbose("Cancelling existing signIn flow") - await sendCancelSignInEvent() - case .signedOut: - return - default: continue - } - } - } - - // MARK: Private helpers - - private func customerClientMetadata() -> [String: String] { - - if let options = request.options.pluginOptions as? AWSAuthSignUpAndSignInPasswordlessOptions, - let clientMetadata = options.clientMetadata { - return clientMetadata - } else if let options = request.options.pluginOptions as? AWSAuthSignInPasswordlessOptions, - let clientMetadata = options.clientMetadata { - return clientMetadata - } - return [:] - } - - private func getCodeDeliveryDetails(parameters: [String: String]) -> AuthCodeDeliveryDetails { - - var deliveryDestination = DeliveryDestination.unknown(nil) - var attribute: AuthUserAttributeKey? = nil - - // Retrieve Delivery medium and destination - let medium = parameters["deliveryMedium"] - let destination = parameters["destination"] - if medium == "SMS" { - deliveryDestination = .sms(destination) - } else if medium == "EMAIL" { - deliveryDestination = .email(destination) - } - - // Retrieve attribute name - if let attributeName = parameters["attributeName"] { - attribute = AuthUserAttributeKey(rawValue: attributeName) - } - - return AuthCodeDeliveryDetails( - destination: deliveryDestination, - attributeKey: attribute) - } - - // MARK: Sign In Cancellation - - private func sendCancelSignInEvent() async { - let event = AuthenticationEvent(eventType: .cancelSignIn) - await authStateMachine.send(event) - } - - private func waitForSignInCancel() async { - let stateSequences = await authStateMachine.listen() - log.verbose("Wait for signIn to cancel") - for await state in stateSequences { - guard case .configured(let authenticationState, _) = state else { - continue - } - switch authenticationState { - case .signedOut: - return - default: continue - } - } + func execute() async throws -> AuthSignInResult { + return try await passwordlessSignInHelper.signIn() } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HelperTests/MagicLinkTokenParserTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HelperTests/MagicLinkTokenParserTests.swift new file mode 100644 index 0000000000..62fee69a7c --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/HelperTests/MagicLinkTokenParserTests.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import XCTest +@testable import AWSCognitoAuthPlugin + +class MagicLinkTokenParserTests: XCTestCase { + + /// Given: A valid Magic Link Token + /// When: MagicLinkTokenParser.extractUserName is invoked + /// Then: A non-empty username string is returned + func testUsernameExtractionSuccess() throws { + let token = "eyJ1c2VybmFtZSI6InRlc3RAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDExODkwODksImV4cCI6MTcwMTE4OTY4OX0.AQIDBAUGBwgJ" + let username = try MagicLinkTokenParser.extractUserName(from: token) + XCTAssertNotNil(username) + XCTAssertEqual(username, "test@example.com") + } + + /// Given: A valid Magic Link Token with empty username + /// When: MagicLinkTokenParser.extractUserName is invoked + /// Then: A empty username string is returned + func testExtractionOfEmptyUsername() throws { + let token = "eyJ1c2VybmFtZSI6IiIsImlhdCI6MTcwMTE4OTIyMiwiZXhwIjoxNzAxMTg5ODIyfQ.AQIDBAUGBwgJ" + let username = try MagicLinkTokenParser.extractUserName(from: token) + XCTAssertNotNil(username) + XCTAssertEqual(username, "") + } + + /// Given: A invalid Magic Link Token + /// When: MagicLinkTokenParser.extractUserName is invoked + /// Then: A SignInError should be thrown with missing username message + func testUsernameExtractionFailureWhenMissingUsername() throws { + let token = "eyJpYXQiOjE3MDExODkyNjcsImV4cCI6MTcwMTE4OTg2N30.AQIDBAUGBwgJ" + do { + _ = try MagicLinkTokenParser.extractUserName(from: token) + XCTFail("Extraction of username should not pass") + } catch SignInError.invalidServiceResponse(let message) { + XCTAssertNotNil(message) + XCTAssertEqual(message, "Did not find username object in magic link token") + } catch { + XCTFail("Error should be of type Sign In Error") + } + } + + /// Given: A invalid Magic Link Token which is malformed + /// When: MagicLinkTokenParser.extractUserName is invoked + /// Then: A SignInError should be thrown with malformed error + func testUsernameExtractionFailureWhenTokenMalformed() throws { + let token = "eyJpYXQiOjE3MDExODkyNjcsImV4cCI6MTcwMTE4OTg2N30" + do { + _ = try MagicLinkTokenParser.extractUserName(from: token) + XCTFail("Extraction of username should not pass") + } catch SignInError.invalidServiceResponse(let message) { + XCTAssertNotNil(message) + XCTAssertEqual(message, "Malformed magic link token") + } catch { + XCTFail("Error should be of type Sign In Error") + } + } + + /// Given: A invalid Magic Link Token with bad data + /// When: MagicLinkTokenParser.extractUserName is invoked + /// Then: A SignInError should be thrown with unable to decode message + func testUsernameExtractionFailureWithBadTokenData() throws { + let token = "badData.badData" + do { + _ = try MagicLinkTokenParser.extractUserName(from: token) + XCTFail("Extraction of username should not pass") + } catch SignInError.invalidServiceResponse(let message) { + XCTAssertNotNil(message) + XCTAssertEqual(message, "Unable to to decode magic link token") + } catch { + XCTFail("Error should be of type Sign In Error") + } + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithMagicLinkTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithMagicLinkTaskTests.swift index f506939d4d..0058ea06d0 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithMagicLinkTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithMagicLinkTaskTests.swift @@ -44,15 +44,29 @@ class AWSAuthConfirmSignInWithMagicLinkTaskTests: BasePluginTest { /// func testSuccessfulConfirmSignInWithMagicLink() async { + let validMagicLinkToken = "eyJ1c2VybmFtZSI6InRlc3RAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDExODkwODksImV4cCI6MTcwMTE4OTY4OX0.AQIDBAUGBwgJ" let customerMetadata = [ "somekey": "somevalue" ] + self.mockIdentityProvider = MockIdentityProvider( + mockInitiateAuthResponse: { input in + XCTAssertEqual(input.clientMetadata?["somekey"], "somevalue") + XCTAssertEqual(input.authParameters?["USERNAME"], "test@example.com") + + return InitiateAuthOutput( + authenticationResult: .none, + challengeName: .customChallenge, + challengeParameters: [ + "nextStep": "PROVIDE_AUTH_PARAMETERS" + ], + session: "someSession") + }, mockRespondToAuthChallengeResponse: { request in XCTAssertEqual(request.challengeName, .customChallenge) - XCTAssertEqual(request.challengeResponses?["ANSWER"], "code") - XCTAssertEqual(request.clientMetadata?["amplify.passwordless.signInMethod"], "MAGIC_LINK") - XCTAssertEqual(request.clientMetadata?["amplify.passwordless.action"], "CONFIRM") + XCTAssertEqual(request.challengeResponses?["ANSWER"], validMagicLinkToken) + XCTAssertEqual(request.clientMetadata?["Amplify.Passwordless.signInMethod"], "MAGIC_LINK") + XCTAssertEqual(request.clientMetadata?["Amplify.Passwordless.action"], "CONFIRM") XCTAssertEqual(request.clientMetadata?["somekey"], "somevalue") return .testData() }) @@ -62,7 +76,7 @@ class AWSAuthConfirmSignInWithMagicLinkTaskTests: BasePluginTest { metadata: customerMetadata) let option = AuthConfirmSignInWithMagicLinkRequest.Options(pluginOptions: confirmSignInOptions) let confirmSignInResult = try await plugin.confirmSignInWithMagicLink( - challengeResponse: "code", + challengeResponse: validMagicLinkToken, options: option) guard case .done = confirmSignInResult.nextStep else { diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithOTPTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithOTPTaskTests.swift index 2d65c8f3f8..70bd752ee3 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithOTPTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthConfirmSignInWithOTPTaskTests.swift @@ -53,8 +53,8 @@ class AWSAuthConfirmSignInWithOTPTaskTests: BasePluginTest { mockRespondToAuthChallengeResponse: { request in XCTAssertEqual(request.challengeName, .customChallenge) XCTAssertEqual(request.challengeResponses?["ANSWER"], "code") - XCTAssertEqual(request.clientMetadata?["amplify.passwordless.signInMethod"], "OTP") - XCTAssertEqual(request.clientMetadata?["amplify.passwordless.action"], "CONFIRM") + XCTAssertEqual(request.clientMetadata?["Amplify.Passwordless.signInMethod"], "OTP") + XCTAssertEqual(request.clientMetadata?["Amplify.Passwordless.action"], "CONFIRM") XCTAssertEqual(request.clientMetadata?["somekey"], "somevalue") return .testData() }) diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithMagicLinkTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithMagicLinkTaskTests.swift index dcc59f6c4b..f509abb1ef 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithMagicLinkTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithMagicLinkTaskTests.swift @@ -40,9 +40,9 @@ class AWSAuthSignInWithMagicLinkTaskTests: BasePluginTest { ], session: "someSession") }, mockRespondToAuthChallengeResponse: { input in - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.signInMethod"], "MAGIC_LINK") - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.action"], "REQUEST") - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.deliveryMedium"], "EMAIL") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.signInMethod"], "MAGIC_LINK") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.action"], "REQUEST") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.deliveryMedium"], "EMAIL") XCTAssertEqual(input.clientMetadata?["somekey"], "somevalue") return RespondToAuthChallengeOutput( diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithOTPTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithOTPTaskTests.swift index 1f06408c55..7b6ed2027d 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithOTPTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/SignIn/AWSAuthSignInWithOTPTaskTests.swift @@ -42,9 +42,9 @@ class AWSAuthSignInWithOTPTaskTests: BasePluginTest { ], session: "someSession") }, mockRespondToAuthChallengeResponse: { input in - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.signInMethod"], "OTP") - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.action"], "REQUEST") - XCTAssertEqual(input.clientMetadata?["amplify.passwordless.deliveryMedium"], "EMAIL") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.signInMethod"], "OTP") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.action"], "REQUEST") + XCTAssertEqual(input.clientMetadata?["Amplify.Passwordless.deliveryMedium"], "EMAIL") XCTAssertEqual(input.clientMetadata?["somekey"], "somevalue") return RespondToAuthChallengeOutput(