Skip to content

Commit

Permalink
feat: Refactored based on TOTP API review (#45)
Browse files Browse the repository at this point in the history
* feat: Converting to a dedicated MFA Selection state

* feat: converting to a dedicated setup totp state and refactoring options

* chore: increasing the tolerance to 1 percent for snapshot testing

* worked on review commetns

* trying out deducing step information when creating a view

* worked on API review changes

* added unit tests

* worked on review comments..

* worked on review comments.
  • Loading branch information
harsh62 authored Oct 25, 2023
1 parent 4385d14 commit dad3823
Show file tree
Hide file tree
Showing 17 changed files with 540 additions and 155 deletions.
63 changes: 30 additions & 33 deletions Sources/Authenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct Authenticator<LoadingContent: View,
SignInContent: View,
ConfirmSignInWithNewPasswordContent: View,
ConfirmSignInWithMFACodeContent: View,
ConfirmSignInWithTOTPContent: View,
ConfirmSignInWithTOTPCodeContent: View,
ContinueSignInWithMFASelectionContent: View,
ContinueSignInWithTOTPSetupContent: View,
ConfirmSignInWithCustomChallengeContent: View,
Expand All @@ -32,15 +32,15 @@ public struct Authenticator<LoadingContent: View,
@State private var currentStep: Step = .loading
@State private var previousStep: Step = .loading
private var initialStep: AuthenticatorInitialStep
private var totpOptions: TOTPOptions?
private var totpOptions: TOTPOptions
private var viewModifiers = ViewModifiers()
private var contentStates: NSHashTable<AuthenticatorBaseState> = .weakObjects()
private let loadingContent: LoadingContent
private let signInContent: SignInContent
private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent
private let confirmSignInWithTOTPContent: ConfirmSignInWithTOTPContent
private let continueSignInWithMFASelectionContent: ContinueSignInWithMFASelectionContent
private let continueSignInWithTOTPSetupContent: ContinueSignInWithTOTPSetupContent
private let confirmSignInWithTOTPCodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent
private let continueSignInWithMFASelectionContent: (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent
private let continueSignInWithTOTPSetupContent: (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent
private let confirmSignInContentWithCustomChallengeContent: ConfirmSignInWithCustomChallengeContent
private let confirmSignInContentWithNewPasswordContent: ConfirmSignInWithNewPasswordContent
private let signUpContent: SignUpContent
Expand Down Expand Up @@ -96,7 +96,7 @@ public struct Authenticator<LoadingContent: View,
/// - Parameter content: The content associated with the ``AuthenticatorStep/signedIn`` step, i.e. once the user has successfully authenticated.
public init(
initialStep: AuthenticatorInitialStep = .signIn,
totpOptions: TOTPOptions? = nil,
totpOptions: TOTPOptions = .init(),
@ViewBuilder loadingContent: () -> LoadingContent = {
ProgressView()
},
Expand All @@ -106,13 +106,13 @@ public struct Authenticator<LoadingContent: View,
@ViewBuilder confirmSignInWithMFACodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithMFACodeContent = { state in
ConfirmSignInWithMFACodeView(state: state)
},
@ViewBuilder confirmSignInWithTOTPContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPContent = { state in
@ViewBuilder confirmSignInWithTOTPCodeContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent = { state in
ConfirmSignInWithTOTPView(state: state)
},
@ViewBuilder continueSignInWithMFASelectionContent: (ConfirmSignInWithCodeState) -> ContinueSignInWithMFASelectionContent = { state in
@ViewBuilder continueSignInWithMFASelectionContent: @escaping (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent = { state in
ContinueSignInWithMFASelectionView(state: state)
},
@ViewBuilder continueSignInWithTOTPSetupContent: (ConfirmSignInWithCodeState) -> ContinueSignInWithTOTPSetupContent = { state in
@ViewBuilder continueSignInWithTOTPSetupContent: @escaping (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent = { state in
ContinueSignInWithTOTPSetupView(state: state)
},
@ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in
Expand Down Expand Up @@ -161,23 +161,9 @@ public struct Authenticator<LoadingContent: View,
confirmSignInWithMFACodeState
)

let confirmSignInWithTOTPState = ConfirmSignInWithCodeState(credentials: credentials)
contentStates.add(confirmSignInWithMFACodeState)
self.confirmSignInWithTOTPContent = confirmSignInWithTOTPContent(
confirmSignInWithTOTPState
)

let continueSignInWithMFASelectionState = ConfirmSignInWithCodeState(credentials: credentials)
contentStates.add(continueSignInWithMFASelectionState)
self.continueSignInWithMFASelectionContent = continueSignInWithMFASelectionContent(
continueSignInWithMFASelectionState
)

let continueSignInWithTOTPSetupState = ConfirmSignInWithCodeState(credentials: credentials)
contentStates.add(continueSignInWithTOTPSetupState)
self.continueSignInWithTOTPSetupContent = continueSignInWithTOTPSetupContent(
continueSignInWithTOTPSetupState
)
self.confirmSignInWithTOTPCodeContent = confirmSignInWithTOTPCodeContent
self.continueSignInWithMFASelectionContent = continueSignInWithMFASelectionContent
self.continueSignInWithTOTPSetupContent = continueSignInWithTOTPSetupContent

let confirmSignInWithCustomChallengeState = ConfirmSignInWithCodeState(credentials: credentials)
contentStates.add(confirmSignInWithMFACodeState)
Expand Down Expand Up @@ -241,7 +227,6 @@ public struct Authenticator<LoadingContent: View,
}
.animation(viewModifiers.contentAnimation, value: currentStep)
.environment(\.authenticatorOptions.hidesSignUpButton, viewModifiers.hidesSignUpButton)
.environment(\.authenticatorOptions.totpOptions, totpOptions)
.environment(\.authenticatorOptions.contentAnimation, viewModifiers.contentAnimation)
.environment(\.authenticatorOptions.contentTransition, viewModifiers.contentTransition)
.environment(\.authenticatorOptions.signUpFields, viewModifiers.signUpFields)
Expand Down Expand Up @@ -347,12 +332,24 @@ public struct Authenticator<LoadingContent: View,
confirmSignInContentWithNewPasswordContent
case .confirmSignInWithMFACode:
confirmSignInContentWithMFACodeContent
case .continueSignInWithMFASelection:
continueSignInWithMFASelectionContent
case .confirmSignInWithTOTP:
confirmSignInWithTOTPContent
case .continueSignInWithTOTPSetup:
continueSignInWithTOTPSetupContent
case .continueSignInWithMFASelection(let allowedMFATypes):
let continueSignInWithMFASelection = ContinueSignInWithMFASelectionState(
authenticatorState: state,
allowedMFATypes: allowedMFATypes
)
continueSignInWithMFASelectionContent(continueSignInWithMFASelection)
case .confirmSignInWithTOTPCode:
let confirmSignInWithCodeState = ConfirmSignInWithCodeState(
authenticatorState: state
)
confirmSignInWithTOTPCodeContent(confirmSignInWithCodeState)
case .continueSignInWithTOTPSetup(let totpSetupDetails):
let totpStupState = ContinueSignInWithTOTPSetupState(
authenticatorState: state,
issuer: totpOptions.issuer,
totpSetupDetails: totpSetupDetails
)
continueSignInWithTOTPSetupContent(totpStupState)
case .confirmSignInWithCustomChallenge:
confirmSignInContentWithCustomChallengeContent
case .resetPassword:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import SwiftUI

class AuthenticatorOptions: ObservableObject {
@Published var totpOptions: TOTPOptions? = nil
@Published var hidesSignUpButton = false
@Published var contentAnimation: Animation = .easeInOut(duration: 0.25)
@Published var contentTransition: AnyTransition = .opacity
Expand Down
1 change: 0 additions & 1 deletion Sources/Authenticator/Extensions/Bundle+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Foundation

extension Bundle {

// Name of the app
var applicationName: String? {
if let localizedName = Bundle.main.infoDictionary?[kCFBundleLocalizationsKey as String] as? String {
return localizedName
Expand Down
2 changes: 1 addition & 1 deletion Sources/Authenticator/Models/AuthenticatorStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct AuthenticatorStep: Equatable {

/// A user has successfully provided valid Sign In credentials but is required TOTP code from their associated authenticator token generator
/// so they are presented with the Confirm Sign In with TOTP Code View
public static let confirmSignInWithTOTP = AuthenticatorStep("confirmSignInWithTOTP")
public static let confirmSignInWithTOTPCode = AuthenticatorStep("confirmSignInWithTOTPCode")

/// A user has successfully provided valid Sign In credentials but is required to setup TOTP before continuing sign in
/// so they are presented with the TOTP Setup View
Expand Down
8 changes: 4 additions & 4 deletions Sources/Authenticator/Models/Internal/Step.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum Step {
case error(_ error: Error)
case signIn
case confirmSignInWithCustomChallenge
case confirmSignInWithTOTP
case confirmSignInWithTOTPCode
case continueSignInWithMFASelection(allowedMFATypes: AllowedMFATypes)
case continueSignInWithTOTPSetup(totpSetupDetails: TOTPSetupDetails)
case confirmSignInWithMFACode(deliveryDetails: AuthCodeDeliveryDetails?)
Expand Down Expand Up @@ -49,8 +49,8 @@ enum Step {
return .signIn
case .confirmSignInWithCustomChallenge:
return .confirmSignInWithCustomChallenge
case .confirmSignInWithTOTP:
return .confirmSignInWithTOTP
case .confirmSignInWithTOTPCode:
return .confirmSignInWithTOTPCode
case .continueSignInWithTOTPSetup:
return .continueSignInWithTOTPSetup
case .continueSignInWithMFASelection:
Expand Down Expand Up @@ -84,7 +84,7 @@ extension Step: Equatable {
(.error, .error),
(.signIn, .signIn),
(.continueSignInWithMFASelection, .continueSignInWithMFASelection),
(.confirmSignInWithTOTP, .confirmSignInWithTOTP),
(.confirmSignInWithTOTPCode, .confirmSignInWithTOTPCode),
(.continueSignInWithTOTPSetup, .continueSignInWithTOTPSetup),
(.confirmSignInWithCustomChallenge, .confirmSignInWithCustomChallenge),
(.confirmSignInWithNewPassword, .confirmSignInWithNewPassword),
Expand Down
4 changes: 2 additions & 2 deletions Sources/Authenticator/Options/TOTPOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ public struct TOTPOptions {
/// account name. In most cases, this should be the name of your app.
/// For example, if your app is called "My App", your user will see
/// "My App" - "username" in their TOTP app.
public let issuer: String
public let issuer: String?

/// Creates a `TOTPOptions`
/// - Parameter issuer: The `issuer` is the title displayed in a user's TOTP App
public init(issuer: String) {
public init(issuer: String? = nil) {
self.issuer = issuer
}
}
8 changes: 7 additions & 1 deletion Sources/Authenticator/States/AuthenticatorBaseState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public class AuthenticatorBaseState: ObservableObject {
self.credentials = credentials
}

init(authenticatorState: AuthenticatorStateProtocol,
credentials: Credentials) {
self.authenticatorState = authenticatorState
self.credentials = credentials
}

func configure(with authenticatorState: AuthenticatorStateProtocol) {
self.authenticatorState = authenticatorState
}
Expand Down Expand Up @@ -105,7 +111,7 @@ public class AuthenticatorBaseState: ObservableObject {
return .verifyUser(attributes: unverifiedAttributes)
}
case .confirmSignInWithTOTPCode:
return .confirmSignInWithTOTP
return .confirmSignInWithTOTPCode
case .continueSignInWithMFASelection(let allowedMFATypes):
return .continueSignInWithMFASelection(allowedMFATypes: allowedMFATypes)
case .continueSignInWithTOTPSetup(let totpSetupDetails):
Expand Down
29 changes: 10 additions & 19 deletions Sources/Authenticator/States/ConfirmSignInWithCodeState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,22 @@ public class ConfirmSignInWithCodeState: AuthenticatorBaseState {
/// The confirmation code provided by the user
@Published public var confirmationCode: String = ""

/// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmSignInWithMFACode` step, it returns `nil`
public var deliveryDetails: AuthCodeDeliveryDetails? {
guard case .confirmSignInWithMFACode(let deliveryDetails) = authenticatorState.step else {
return nil
}

return deliveryDetails
override init(credentials: Credentials) {
super.init(credentials: credentials)
}

/// The `Amplify.AllowedMFATypes` associated with this state. If the Authenticator is not in the `.continueSignInWithMFASelection` step, it returns `empty` result
public var allowedMFATypes: AllowedMFATypes {
guard case .continueSignInWithMFASelection(let allowedMFATypes) = authenticatorState.step else {
return []
}

return allowedMFATypes
init(authenticatorState: AuthenticatorStateProtocol) {
super.init(authenticatorState: authenticatorState,
credentials: Credentials())
}

/// The `Amplify.TOTPSetupDetails` associated with this state. If the Authenticator is not in the `.continueSignInWithTOTPSetup` step, it returns `nil` result
public var totpSetupDetails: TOTPSetupDetails? {
guard case .continueSignInWithTOTPSetup(let totpSetupDetails) = authenticatorState.step else {
/// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmSignInWithMFACode` step, it returns `nil`
public var deliveryDetails: AuthCodeDeliveryDetails? {
guard case .confirmSignInWithMFACode(let deliveryDetails) = authenticatorState.step else {
return nil
}

return totpSetupDetails
return deliveryDetails
}

/// Attempts to confirm the user's sign in using the provided confirmation code.
Expand All @@ -50,7 +41,7 @@ public class ConfirmSignInWithCodeState: AuthenticatorBaseState {
setBusy(true)

do {
log.verbose("Attempting to confirm Sign Up")
log.verbose("Attempting to confirm Sign In with Code")
let result = try await authenticationService.confirmSignIn(
challengeResponse: confirmationCode,
options: nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import AWSCognitoAuthPlugin
import SwiftUI

/// The state observed by the Continue Sign In With MFA Selection content views, representing the ``Authenticator`` is in ``AuthenticatorStep/continueSignInWithMFASelection`` step.
public class ContinueSignInWithMFASelectionState: AuthenticatorBaseState {

/// The confirmation code provided by the user
@Published public var selectedMFAType: MFAType?

init(authenticatorState: AuthenticatorStateProtocol,
allowedMFATypes: AllowedMFATypes) {
self.allowedMFATypes = allowedMFATypes
super.init(authenticatorState: authenticatorState,
credentials: Credentials())
}

/// The `Amplify.AllowedMFATypes` associated with this state.
public let allowedMFATypes: AllowedMFATypes

/// Attempts to continue the user's sign in using the provided confirmation code.
///
/// Automatically sets the Authenticator's next step accordingly, as well as the
/// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
/// - Throws: An `Amplify.AuthenticationError` if the operation fails
public func continueSignIn() async throws {
guard let selectedMFAType = selectedMFAType else {
log.error("MFA type not selected")
return
}

setBusy(true)
do {
log.verbose("Attempting to confirm Sign In with Code")
let result = try await authenticationService.confirmSignIn(
challengeResponse: selectedMFAType.challengeResponse,
options: nil
)
let nextStep = try await nextStep(for: result)

setBusy(false)

authenticatorState.setCurrentStep(nextStep)
} catch {
log.error("Confirm Sign In with Code failed")
let authenticationError = self.error(for: error)
setMessage(authenticationError)
throw authenticationError
}
}
}
Loading

0 comments on commit dad3823

Please sign in to comment.