Skip to content

Commit

Permalink
feat(Auth): Add Passwordless Sign In with OTP implementation (#3386)
Browse files Browse the repository at this point in the history
* feat(Auth): Add Passwordless Sign In with OTP implementation

* update namespacing

* added new options

* worked on review comments
  • Loading branch information
harsh62 authored and thisisabhash committed Dec 13, 2023
1 parent 0e3e247 commit 5530f7b
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 66 deletions.
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,18 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify

//[HS]: TODO: Make sure this is approved API for options
public struct AWSAuthConfirmSignInWithMagicLinkOptions {

/// 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,18 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify

//[HS]: TODO: Make sure this is approved API for options
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

0 comments on commit 5530f7b

Please sign in to comment.