Skip to content

Commit

Permalink
feat(Auth): Add OTP API's and Tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
harsh62 committed Nov 27, 2023
1 parent 19ade33 commit 730c5a2
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 8 deletions.
32 changes: 30 additions & 2 deletions Amplify/Categories/Auth/AuthCategory+ClientBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,41 @@ extension AuthCategory: AuthCategoryBehavior {
redirectURL: String,
options: AuthPasswordlessMagicLinkRequest.Options? = nil
) async throws -> AuthSignInResult {
try await plugin.signInWithMagicLink(username: username, flow: flow, redirectURL: redirectURL, options: options)
try await plugin.signInWithMagicLink(
username: username,
flow: flow,
redirectURL: redirectURL,
options: options)
}

public func confirmSignInWithMagicLink(
challengeResponse: String,
options: AuthPasswordlessMagicLinkRequest.Options? = nil
) async throws -> AuthSignInResult {
try await plugin.confirmSignInWithMagicLink(challengeResponse: challengeResponse, options: options)
try await plugin.confirmSignInWithMagicLink(
challengeResponse: challengeResponse,
options: options)
}

public func signInWithOTP(
username: String,
flow: AuthPasswordlessFlow,
destination: AuthPasswordlessDeliveryDestination,
options: AuthSignInWithOTPRequest.Options? = nil
) async throws -> AuthSignInResult {
try await plugin.signInWithOTP(
username: username,
flow: flow,
destination: destination,
options: options)
}

public func confirmSignInWithOTP(
challengeResponse: String,
options: AuthConfirmSignInWithOTPRequest.Options? = nil
) async throws -> AuthSignInResult {
try await plugin.confirmSignInWithOTP(
challengeResponse: challengeResponse,
options: options)
}
}
28 changes: 28 additions & 0 deletions Amplify/Categories/Auth/AuthCategoryBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,32 @@ public protocol AuthCategoryBehavior: AuthCategoryUserBehavior, AuthCategoryDevi
options: AuthPasswordlessMagicLinkRequest.Options?
) async throws -> AuthSignInResult

/// Initiates Sign in with OTP
///
/// Invoke this operation to start sign in with Magic Link flow
///
/// - Parameters:
/// - username: username used for sign in
/// - flow: `AuthPasswordlessFlow` type - can be `.signUpAndSignIn` or `.signIn`
/// - destination: `AuthPasswordlessDeliveryDestination` type - can be `sms` or `email`
/// - options: Parameters specific to plugin behavior
func signInWithOTP(
username: String,
flow: AuthPasswordlessFlow,
destination: AuthPasswordlessDeliveryDestination,
options: AuthSignInWithOTPRequest.Options?
) async throws -> AuthSignInResult

/// Confirms Sign in for OTP flow
///
/// Invoke this operation to confirm sign in with OTP flow and sign in the user
///
/// - Parameters:
/// - challengeResponse: challengeResponse received as a OTP on the destination provided during sign in
/// - options: Parameters specific to plugin behavior
func confirmSignInWithOTP(
challengeResponse: String,
options: AuthConfirmSignInWithOTPRequest.Options?
) async throws -> AuthSignInResult

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@

import Foundation

/// Request to sign in a user with Passwordless OTP flow
public struct AuthPasswordlessOTPRequest: AmplifyOperationRequest {
/// Request to confirm sign in a user with OTP flow
public struct AuthConfirmSignInWithOTPRequest: AmplifyOperationRequest {

/// The value of `challengeResponse`is the OTP that is received on the destination provided during sign in request
public let challengeResponse: String

/// Extra request options defined in `AuthPasswordlessOTPRequest.Options`
public var options: Options

public init(options: Options) {
public init(challengeResponse: String, options: Options) {
self.challengeResponse = challengeResponse
self.options = options
}
}

public extension AuthPasswordlessOTPRequest {
public extension AuthConfirmSignInWithOTPRequest {

struct Options {

Expand Down
49 changes: 49 additions & 0 deletions Amplify/Categories/Auth/Request/AuthSignInWithOTPRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Request to sign in a user with Passwordless OTP flow
public struct AuthSignInWithOTPRequest: AmplifyOperationRequest {

/// User name for which the passwordless OTP was initiated
public let username: String

/// The flow that the request should begin with.
public let flow: AuthPasswordlessFlow

/// The destination where the OTP will be sent
public let destination: AuthPasswordlessDeliveryDestination

/// Extra request options defined in `AuthSignInWithOTPRequest.Options`
public var options: Options

public init(username: String,
flow: AuthPasswordlessFlow,
destination: AuthPasswordlessDeliveryDestination,
options: Options) {
self.username = username
self.flow = flow
self.destination = destination
self.options = options
}
}

public extension AuthSignInWithOTPRequest {

struct Options {

/// Extra plugin specific options, only used in special circumstances when the existing options do not provide
/// a way to utilize the underlying auth plugin functionality. See plugin documentation for expected
/// key/values
public let pluginOptions: Any?

public init(pluginOptions: Any? = nil) {
self.pluginOptions = pluginOptions
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,43 @@ extension AWSCognitoAuthPlugin: AuthCategoryBehavior {
) async throws -> AuthSignInResult {
throw AuthError.unknown("Not Implemented")
}

public func signInWithOTP(
username: String,
flow: AuthPasswordlessFlow,
destination: AuthPasswordlessDeliveryDestination,
options: AuthSignInWithOTPRequest.Options?
) async throws -> AuthSignInResult {
let options = options ?? AuthSignInWithOTPRequest.Options()
let request = AuthSignInWithOTPRequest(
username: username,
flow: flow,
destination: destination,
options: options)
let task = AWSAuthSignInWithOTPTask(
request,
authStateMachine: self.authStateMachine,
configuration: authConfiguration)
return try await taskQueue.sync {
return try await task.value
} as! AuthSignInResult
}

public func confirmSignInWithOTP(
challengeResponse: String,
options: AuthConfirmSignInWithOTPRequest.Options?
) async throws -> AuthSignInResult {

let options = options ?? AuthConfirmSignInWithOTPRequest.Options()
let request = AuthConfirmSignInWithOTPRequest(
challengeResponse: challengeResponse,
options: options)
let task = AWSAuthConfirmSignInWithOTPTask(
request,
stateMachine: authStateMachine,
configuration: authConfiguration)
return try await taskQueue.sync {
return try await task.value
} as! AuthSignInResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ struct ConfirmSignInEventData {
let metadata: [String: String]?
let friendlyDeviceName: String?

init(answer: String,
attributes: [String : String] = [:],
metadata: [String : String]? = nil,
friendlyDeviceName: String? = nil) {
self.answer = answer
self.attributes = attributes
self.metadata = metadata
self.friendlyDeviceName = friendlyDeviceName
}

}

extension ConfirmSignInEventData: Equatable { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify

extension AuthConfirmSignInWithOTPRequest {

func hasError() -> AuthError? {
guard !challengeResponse.isEmpty else {
return AuthError.validation(
AuthPluginErrorConstants.confirmSignInChallengeResponseError.field,
AuthPluginErrorConstants.confirmSignInChallengeResponseError.errorDescription,
AuthPluginErrorConstants.confirmSignInChallengeResponseError.recoverySuggestion)
}
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import Amplify
import AWSPluginsCore

class AWSAuthConfirmSignInWithOTPTask: AuthConfirmSignInWithOTPTask, DefaultLogger {

private let request: AuthConfirmSignInWithOTPRequest
private let authStateMachine: AuthStateMachine
private let taskHelper: AWSAuthTaskHelper
private let authConfiguration: AuthConfiguration

var eventName: HubPayloadEventName {
HubPayload.EventName.Auth.confirmSignInWithOTPAPI
}

init(_ request: AuthConfirmSignInWithOTPRequest,
stateMachine: AuthStateMachine,
configuration: AuthConfiguration) {
self.request = request
self.authStateMachine = stateMachine
self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine)
self.authConfiguration = configuration
}

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
}
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
}

// [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
}

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 {

// [HS] TODO: Confirm if any metadata needs to be passed during confirm sign in
/*
* Attributes
* Metadata
* Device Name
*/
return ConfirmSignInEventData(
answer: self.request.challengeResponse)
}
}
Loading

0 comments on commit 730c5a2

Please sign in to comment.