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 OTP API's and Tasks #3381

Merged
merged 2 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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
thisisabhash marked this conversation as resolved.
Show resolved Hide resolved

/// 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()))
thisisabhash marked this conversation as resolved.
Show resolved Hide resolved
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
Loading