Skip to content

Commit

Permalink
feat(auth): add support for passwordless sign up and auto sign in (#160)
Browse files Browse the repository at this point in the history
* add autoSignIn() category API definitions (#152)

* add autoSignIn() category API definitions

* add sign up step for auto sign in

* add state machine changes for autoSignIn() and signUp() (#154)

* add autoSignIn() category API definitions

* add sign up step for auto sign in

* add state machine changes

* add events and update resolvers

* update sign up events and resolvers

* add updates to resolver for auto sign in

* update confirm sign up flow and debug code

* Address review comments

---------

Co-authored-by: Harsh <[email protected]>

* update auto sign state machine events and resolver (#157)

* update auto sign state machine events and resolver

* Address review comments

* update sign up and auto sign in unit tests (#159)

* update sign up and auto sign in unit tests

* add auto sign in tests and refactor existing tests

* Add more service error tests

* Address review changes

---------

Co-authored-by: Harsh <[email protected]>
  • Loading branch information
thisisabhash and harsh62 committed Nov 3, 2024
1 parent a6e760b commit 364b966
Show file tree
Hide file tree
Showing 76 changed files with 1,940 additions and 327 deletions.
4 changes: 4 additions & 0 deletions Amplify/Categories/Auth/AuthCategory+ClientBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ extension AuthCategory: AuthCategoryBehavior {
return await plugin.signOut(options: options)
}

public func autoSignIn() async throws -> AuthSignInResult {
try await plugin.autoSignIn()
}

public func deleteUser() async throws {
try await plugin.deleteUser()
}
Expand Down
4 changes: 4 additions & 0 deletions Amplify/Categories/Auth/AuthCategoryBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ public protocol AuthCategoryBehavior: AuthCategoryUserBehavior, AuthCategoryDevi
options: AuthConfirmSignInRequest.Options?
) async throws -> AuthSignInResult


/// Auto signs in the user for passwordless sign up
func autoSignIn() async throws -> AuthSignInResult

/// Sign out the currently logged-in user.
///
/// - Parameters:
Expand Down
5 changes: 5 additions & 0 deletions Amplify/Categories/Auth/Models/AuthSignUpStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

public typealias UserId = String
public typealias Session = String

/// SignUp step to be followed.
public enum AuthSignUpStep {
Expand All @@ -16,6 +17,10 @@ public enum AuthSignUpStep {
AdditionalInfo? = nil,
UserId? = nil)

/// Sign Up successfully completed
/// The customers can use this step to determine if they want to complete sign in
case completeAutoSignIn(Session)

/// Sign up is complete
case done
}
34 changes: 34 additions & 0 deletions Amplify/Categories/Auth/Request/AuthAutoSignInRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Request to auto sign in
public struct AuthAutoSignInRequest: AmplifyOperationRequest {

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

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

public extension AuthAutoSignInRequest {

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
}
}
}
2 changes: 1 addition & 1 deletion Amplify/Categories/Auth/Result/AuthSignUpResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct AuthSignUpResult {
/// Indicate whether the signUp flow is completed.
public var isSignUpComplete: Bool {
switch nextStep {
case .done:
case .completeAutoSignIn, .done:
return true
default:
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct ClearFederationToIdentityPool: Action {
try await credentialStoreClient?.deleteData(type: .amplifyCredentials)
event = AuthenticationEvent.init(eventType: .clearedFederationToIdentityPool)
} else {
let error = AuthenticationError.service(message: "Unable to find credentials to clear for federation.")
let error = AuthenticationError.service(message: "Unable to find credentials to clear for federation.", error: nil)
event = AuthenticationEvent.init(eventType: .error(error))
logError("\(#fileID) Sending event \(event.type) with error \(error)", environment: environment)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct InitializeSignInFlow: Action {
var identifier: String = "IntializeSignInFlow"

let signInEventData: SignInEventData

let autoSignIn: Bool

func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) async {
logVerbose("\(#fileID) Starting execution", environment: environment)
Expand Down Expand Up @@ -66,7 +68,11 @@ struct InitializeSignInFlow: Action {
case .custom:
return .init(eventType: .initiateCustomSignInWithSRP(signInEventData, deviceMetadata))
case .userAuth:
return .init(eventType: .initiateUserAuth(signInEventData, deviceMetadata))
if autoSignIn {
return .init(eventType: .initiateAutoSignIn(signInEventData, deviceMetadata))
} else {
return .init(eventType: .initiateUserAuth(signInEventData, deviceMetadata))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct ThrowSignInError: Action {

logVerbose("\(#fileID) Starting execution", environment: environment)
let event = AuthenticationEvent(
eventType: .error(.service(message: "\(error)")))
eventType: .error(.service(message: "\(error)", error: error)))
logVerbose("\(#fileID) Sending event \(event)", environment: environment)
await dispatcher.send(event)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation
import AWSCognitoIdentityProvider

struct AutoSignIn: Action {

var identifier: String = "AutoSignIn"
let signInEventData: SignInEventData
let deviceMetadata: DeviceMetadata

func execute(withDispatcher dispatcher: any EventDispatcher, environment: any Environment) async {
do {
let userPoolEnv = try environment.userPoolEnvironment()
let authEnv = try environment.authEnvironment()

guard let username = signInEventData.username else {
logVerbose("\(#fileID) Unable to extract username from signInEventData", environment: environment)
let authError = SignInError.inputValidation(field: "Unable to extract username")
let event = SignInEvent(
eventType: .throwAuthError(authError)
)
await dispatcher.send(event)
return
}

var authParameters = [
"USERNAME": username
]

let configuration = userPoolEnv.userPoolConfiguration
let userPoolClientId = configuration.clientId

if let clientSecret = configuration.clientSecret {
let clientSecretHash = ClientSecretHelper.clientSecretHash(
username: username,
userPoolClientId: userPoolClientId,
clientSecret: clientSecret
)
authParameters["SECRET_HASH"] = clientSecretHash
}

if case .metadata(let data) = deviceMetadata {
authParameters["DEVICE_KEY"] = data.deviceKey
}

let asfDeviceId = try await CognitoUserPoolASF.asfDeviceID(
for: username,
credentialStoreClient: authEnv.credentialsClient)

var userContextData: CognitoIdentityProviderClientTypes.UserContextDataType?
if let encodedData = await CognitoUserPoolASF.encodedContext(
username: username,
asfDeviceId: asfDeviceId,
asfClient: userPoolEnv.cognitoUserPoolASFFactory(),
userPoolConfiguration: configuration) {
userContextData = .init(encodedData: encodedData)
}
let analyticsMetadata = userPoolEnv
.cognitoUserPoolAnalyticsHandlerFactory()
.analyticsMetadata()

let request = InitiateAuthInput(
analyticsMetadata: analyticsMetadata,
authFlow: .userAuth,
authParameters: authParameters,
clientId: userPoolClientId,
clientMetadata: signInEventData.clientMetadata,
session: signInEventData.session,
userContextData: userContextData
)

let responseEvent = try await sendRequest(
request: request,
username: username,
environment: userPoolEnv)
logVerbose("\(#fileID) Sending event \(responseEvent)", environment: environment)
await dispatcher.send(responseEvent)

} catch let error as SignInError {
logVerbose("\(#fileID) Raised error \(error)", environment: environment)
let event = SignInEvent(eventType: .throwAuthError(error))
await dispatcher.send(event)
} catch {
logVerbose("\(#fileID) Caught error \(error)", environment: environment)
let authError = SignInError.service(error: error)
let event = SignInEvent(
eventType: .throwAuthError(authError)
)
await dispatcher.send(event)
}
}

private func sendRequest(request: InitiateAuthInput,
username: String,
environment: UserPoolEnvironment) async throws -> StateMachineEvent {

let cognitoClient = try environment.cognitoUserPoolFactory()
logVerbose("\(#fileID) Starting execution", environment: environment)

let response = try await cognitoClient.initiateAuth(input: request)
return UserPoolSignInHelper.parseResponse(
response,
for: username,
signInMethod: signInEventData.signInMethod,
presentationAnchor: signInEventData.presentationAnchor
)
}
}

extension AutoSignIn: CustomDebugDictionaryConvertible {
var debugDictionary: [String: Any] {
[
"identifier": identifier,
"signInEventData": signInEventData.debugDictionary
]
}
}

extension AutoSignIn: CustomDebugStringConvertible {
var debugDescription: String {
debugDictionary.debugDescription
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation
import AWSCognitoIdentityProvider

struct ConfirmSignUp: Action {

var identifier: String = "ConfirmSignUp"
let data: SignUpEventData
let confirmationCode: String
let forceAliasCreation: Bool?

func execute(withDispatcher dispatcher: any EventDispatcher, environment: any Environment) async {
do {
let authEnvironment = try environment.authEnvironment()
let userPoolEnvironment = authEnvironment.userPoolEnvironment
let asfDeviceId = try await CognitoUserPoolASF.asfDeviceID(
for: data.username,
credentialStoreClient: authEnvironment.credentialsClient)
let client = try userPoolEnvironment.cognitoUserPoolFactory()
let input = await ConfirmSignUpInput(username: data.username,
confirmationCode: confirmationCode,
clientMetadata: data.clientMetadata,
asfDeviceId: asfDeviceId,
forceAliasCreation: forceAliasCreation,
session: data.session,
environment: userPoolEnvironment)
let response = try await client.confirmSignUp(input: input)
let dataToSend = SignUpEventData(
username: data.username,
clientMetadata: data.clientMetadata,
validationData: data.validationData,
session: response.session
)
logVerbose("\(#fileID) ConfirmSignUp response succcess", environment: environment)

if let session = response.session {
await dispatcher.send(SignUpEvent(eventType: .signedUp(dataToSend, .init(.completeAutoSignIn(session)))))
} else {
await dispatcher.send(SignUpEvent(eventType: .signedUp(dataToSend, .init(.done))))
}
} catch let error as SignUpError {
let errorEvent = SignUpEvent(eventType: .throwAuthError(error))
logVerbose("\(#fileID) Sending event \(errorEvent)",
environment: environment)
await dispatcher.send(errorEvent)
} catch {
let error = SignUpError.service(error: error)
let errorEvent = SignUpEvent(eventType: .throwAuthError(error))
logVerbose("\(#fileID) Sending event \(errorEvent)",
environment: environment)
await dispatcher.send(errorEvent)
}
}
}

extension ConfirmSignUp: CustomDebugDictionaryConvertible {
var debugDictionary: [String: Any] {
[
"identifier": identifier,
"signUpEventData": data.debugDictionary,
"confirmationCode": confirmationCode.masked(),
"forceAliasCreation": forceAliasCreation
]
}
}

extension ConfirmSignUp: CustomDebugStringConvertible {
var debugDescription: String {
debugDictionary.debugDescription
}
}
Loading

0 comments on commit 364b966

Please sign in to comment.