Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Auth): Add Passwordless Sign In with OTP implementation #3386

Merged
merged 4 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

/// Delivery destination for the Auth Passwordless flows
///
public enum AuthPasswordlessDeliveryDestination {
case sms
case email
public enum AuthPasswordlessDeliveryDestination: String {
case sms = "SMS"
case email = "EMAIL"
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public struct AWSAuthConfirmSignInOptions {
/// User attributes to be passed in when confirming a sign with NEW_PASSWORD_REQUIRED challenge
public let userAttributes: [AuthUserAttribute]?

/// A map of custom key-value pairs that you can provide as input for any custom workflows that this action triggers.
public let metadata: [String: String]?

/// Device name that would be provided to Cognito when setting up TOTP
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify

public struct AWSAuthConfirmSignInWithMagicLinkOptions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be we can add a todo here once we finalize on the options

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TODO.


/// A map of custom key-value pairs that you can provide as input for any custom workflows that this action triggers.
public let metadata: [String: String]?

public init(metadata: [String: String]? = nil) {
self.metadata = metadata
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify

public struct AWSAuthConfirmSignInWithOTPOptions {

/// A map of custom key-value pairs that you can provide as input for any custom workflows that this action triggers.
public let metadata: [String: String]?

public init(metadata: [String: String]? = nil) {
self.metadata = metadata
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

enum PasswordlessCustomAuthNextStep: String {

case provideAuthParameters = "PROVIDE_AUTH_PARAMETERS"

case provideChallengeResponse = "PROVIDE_CHALLENGE_RESPONSE"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation

enum PasswordlessCustomAuthSignInMethod: String {
case otp = "OTP"
case magicLink = "MAGIC_LINK"
}

enum PasswordlessCustomAuthRequestAction: String {
case request = "REQUEST"
case confirm = "CONFIRM"
}

struct PasswordlessCustomAuthRequest {

private let namespace = "Amplify.Passwordless"

let signInMethod: PasswordlessCustomAuthSignInMethod
let action: PasswordlessCustomAuthRequestAction
let deliveryMedium: AuthPasswordlessDeliveryDestination?

init(signInMethod: PasswordlessCustomAuthSignInMethod,
action: PasswordlessCustomAuthRequestAction,
deliveryMedium: AuthPasswordlessDeliveryDestination? = nil) {
self.signInMethod = signInMethod
self.action = action
self.deliveryMedium = deliveryMedium
}

func toDictionary() -> [String: String] {
var dictionary = [
namespace + ".signInMethod": signInMethod.rawValue,
namespace + ".action": action.rawValue
]
if let deliveryMedium = deliveryMedium {
dictionary[namespace + ".deliveryMedium"] = deliveryMedium.rawValue
}
return dictionary
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct SignInEventData {
let signInMethod: SignInMethod

init(username: String?,
password: String?,
password: String? = nil,
clientMetadata: [String: String] = [:],
signInMethod: SignInMethod) {
self.username = username
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg
private let authStateMachine: AuthStateMachine
private let taskHelper: AWSAuthTaskHelper
private let authConfiguration: AuthConfiguration
private let confirmSignInRequestMetadata: PasswordlessCustomAuthRequest

var eventName: HubPayloadEventName {
HubPayload.EventName.Auth.confirmSignInWithOTPAPI
Expand All @@ -26,21 +27,13 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg
self.authStateMachine = stateMachine
self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine)
self.authConfiguration = configuration
self.confirmSignInRequestMetadata = .init(signInMethod: .otp, action: .confirm)
}

func execute() async throws -> AuthSignInResult {
log.verbose("Starting execution")
await taskHelper.didStateMachineConfigured()

//Check if we have a user pool configuration
guard authConfiguration.getUserPoolConfiguration() != nil else {
let message = AuthPluginErrorConstants.configurationError
let authError = AuthError.configuration(
"Could not find user pool configuration",
message)
throw authError
}

if let validationError = request.hasError() {
throw validationError
}
Expand All @@ -53,11 +46,6 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg
throw invalidStateError
}

// [HS] TODO: Following implementations need to be complete
// 1. Validate it is the correct state to confirm Sign In With OTP
// 2. Send event
// 3. Listent to events
// 4. Complete the task
guard case .resolvingChallenge(let challengeState, _, _) = signInState else {
throw invalidStateError
}
Expand All @@ -70,7 +58,6 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg
throw invalidStateError
}


let stateSequences = await authStateMachine.listen()
log.verbose("Waiting for response")
for await state in stateSequences {
Expand Down Expand Up @@ -110,14 +97,18 @@ class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogg
}

private func createConfirmSignInEventData() -> ConfirmSignInEventData {

// [HS] TODO: Confirm if any metadata needs to be passed during confirm sign in
/*
* Attributes
* Metadata
* Device Name
*/
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)
answer: self.request.challengeResponse,
metadata: passwordlessMetadata)
}
}
Loading
Loading