Skip to content

Commit

Permalink
feat: Adding TOTP support (#43)
Browse files Browse the repository at this point in the history
* feat: Adding enter TOTP code view (#39)

* feat: Adding enter TOTP code view

* making doc changes

* feat: Add MFA selection view (#40)

* feat: Add MFA selection view

* worked on review comments

* feat: Adding TOTP Setup view during sign in (#41)

* feat: Adding TOTP Setup view during sign in

* removed init and added view modifier for issuer

* worked on review comments and refactored bunch of things to log stuff

* feat: add UI testing module for Authenticator (#42)

* feat: add login for testing snaphshots

* updated the image diff logic

* refactored process argument logic

* renamed and regrouped files

* adding new test case

* adding enter totp view tests

* renaming the utils file

* updated entitlements that are not needed

* adding mfa selection test

* adding totp setup tests

* updates tolerance and image

* restructuring and renaming

* removing the hardcoded test key

* clean up

* feat: Refactored based on TOTP API review (#45)

* 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.

* feat: modifying based on API review feedback (#46)

* worked on review comments.

* fixing macOS build error
  • Loading branch information
harsh62 authored Nov 1, 2023
1 parent fd7e6f8 commit 6ec8a2f
Show file tree
Hide file tree
Showing 58 changed files with 3,005 additions and 30 deletions.
52 changes: 50 additions & 2 deletions Sources/Authenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public struct Authenticator<LoadingContent: View,
SignInContent: View,
ConfirmSignInWithNewPasswordContent: View,
ConfirmSignInWithMFACodeContent: View,
ConfirmSignInWithTOTPCodeContent: View,
ContinueSignInWithMFASelectionContent: View,
ContinueSignInWithTOTPSetupContent: View,
ConfirmSignInWithCustomChallengeContent: View,
SignUpContent: View,
ConfirmSignUpContent: View,
Expand All @@ -29,11 +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 viewModifiers = ViewModifiers()
private var contentStates: NSHashTable<AuthenticatorBaseState> = .weakObjects()
private let loadingContent: LoadingContent
private let signInContent: SignInContent
private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent
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 All @@ -50,13 +57,21 @@ public struct Authenticator<LoadingContent: View,
/// Creates an `Authenticator` component
/// - Parameter initialStep: The initial step displayed to unauthorized users.
/// Defaults to ``AuthenticatorInitialStep/signIn``
/// - Parameter totpOptions: The TOTP Options that would be used by the Authenticator
/// Defaults to ``.init()``
/// - Parameter loadingContent: The content that is associated with the ``AuthenticatorStep/loading`` step.
/// Defaults to a `SwiftUI.ProgressView`.
/// - Parameter signInContent: The content associated with the ``AuthenticatorStep/signIn`` step.
/// Defaults to a ``SignInView``.
/// - Parameter confirmSignInWithMFACodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step.
/// - Parameter confirmSignInWithMFACodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithMFACode`` step.
/// Defaults to a ``ConfirmSignInWithMFACodeView``.
/// - Parameter confirmSignInWithCustomChallengeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithMFACode`` step.
///- Parameter confirmSignInWithTOTPCodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithTOTPCode`` step.
/// Defaults to a ``ConfirmSignInWithMFACodeView``.
///- Parameter continueSignInWithMFASelectionContent: The content associated with the ``AuthenticatorStep/continueSignInWithMFASelection`` step.
/// Defaults to a ``ContinueSignInWithMFASelectionView``.
///- Parameter continueSignInWithTOTPSetupContent: The content associated with the ``AuthenticatorStep/continueSignInWithTOTPSetup`` step.
/// Defaults to a ``ContinueSignInWithTOTPSetupView``.
/// - Parameter confirmSignInWithCustomChallengeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step.
/// Defaults to a ``ConfirmSignInWithCustomChallengeView``.
/// - Parameter confirmSignInWithNewPasswordContent: The content associated with the ``AuthenticatorStep/confirmSignInWithNewPassword`` step.
/// Defaults to a ``ConfirmSignInWithNewPasswordView``.
Expand All @@ -81,6 +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 = .init(),
@ViewBuilder loadingContent: () -> LoadingContent = {
ProgressView()
},
Expand All @@ -90,6 +106,15 @@ public struct Authenticator<LoadingContent: View,
@ViewBuilder confirmSignInWithMFACodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithMFACodeContent = { state in
ConfirmSignInWithMFACodeView(state: state)
},
@ViewBuilder confirmSignInWithTOTPCodeContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent = { state in
ConfirmSignInWithTOTPView(state: state)
},
@ViewBuilder continueSignInWithMFASelectionContent: @escaping (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent = { state in
ContinueSignInWithMFASelectionView(state: state)
},
@ViewBuilder continueSignInWithTOTPSetupContent: @escaping (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent = { state in
ContinueSignInWithTOTPSetupView(state: state)
},
@ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in
ConfirmSignInWithCustomChallengeView(state: state)
},
Expand Down Expand Up @@ -122,6 +147,7 @@ public struct Authenticator<LoadingContent: View,
@ViewBuilder content: @escaping (SignedInState) -> SignedInContent
) {
self.initialStep = initialStep
self.totpOptions = totpOptions
self.loadingContent = loadingContent()
let credentials = Credentials()

Expand All @@ -135,6 +161,10 @@ public struct Authenticator<LoadingContent: View,
confirmSignInWithMFACodeState
)

self.confirmSignInWithTOTPCodeContent = confirmSignInWithTOTPCodeContent
self.continueSignInWithMFASelectionContent = continueSignInWithMFASelectionContent
self.continueSignInWithTOTPSetupContent = continueSignInWithTOTPSetupContent

let confirmSignInWithCustomChallengeState = ConfirmSignInWithCodeState(credentials: credentials)
contentStates.add(confirmSignInWithMFACodeState)
self.confirmSignInContentWithCustomChallengeContent = confirmSignInWithCustomChallengeContent(
Expand Down Expand Up @@ -302,6 +332,24 @@ public struct Authenticator<LoadingContent: View,
confirmSignInContentWithNewPasswordContent
case .confirmSignInWithMFACode:
confirmSignInContentWithMFACodeContent
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
24 changes: 24 additions & 0 deletions Sources/Authenticator/Extensions/Bundle+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

extension Bundle {

var applicationName: String? {
if let localizedName = Bundle.main.infoDictionary?[kCFBundleLocalizationsKey as String] as? String {
return localizedName
}
if let displayName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String {
return displayName
}
if let bundleName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String {
return bundleName
}
return nil
}
}
30 changes: 23 additions & 7 deletions Sources/Authenticator/Models/AuthenticatorStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,27 @@ public struct AuthenticatorStep: Equatable {
/// An unauthenticated user is presented with the Sing In view
public static let signIn = AuthenticatorStep("signIn")

/// A user has successfuly provided valid Sign In credentials but is required to provide an additional custom verification response,
/// A user has successfully provided valid Sign In credentials but is required to provide an additional custom verification response,
/// so they are presented with the Confirm Sign In with Custom Challenge view
public static let confirmSignInWithCustomChallenge = AuthenticatorStep("confirmSignInWithCustomChallenge")

/// A user has successfuly provided valid Sign In credentials but is required to provide a MFA code,
/// 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 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
public static let continueSignInWithTOTPSetup = AuthenticatorStep("continueSignInWithTOTPSetup")

/// A user has successfully provided valid Sign In credentials but is required to select a MFA type to continue
/// so they are presented with the Confirm Sign In with MFA Selection View
public static let continueSignInWithMFASelection = AuthenticatorStep("continueSignInWithMFASelection")

/// A user has successfully provided valid Sign In credentials but is required to provide a MFA code,
/// so they are presented with the Confirm Sign In with MFA Code view
public static let confirmSignInWithMFACode = AuthenticatorStep("confirmSignInWithMFACode")

/// A user has sucessfuly provided valid Sign In credentials but is required to change their password,
/// A user has successfully provided valid Sign In credentials but is required to change their password,
/// so they are presented with the Confirm Sign In with New Password view
public static let confirmSignInWithNewPassword = AuthenticatorStep("confirmSignInWithNewPassword")

Expand All @@ -65,18 +77,22 @@ public struct AuthenticatorStep: Equatable {
/// An unauthenticated user is presented with the Reset Password view
public static let resetPassword = AuthenticatorStep("resetPassword")

/// An unauthenticated user successfuly requested a Password Reset and they need to provide a verification code along their new password,
/// An unauthenticated user successfully requested a Password Reset and they need to provide a verification code along their new password,
/// so they are presented with the Confirm Reset Password view
public static let confirmResetPassword = AuthenticatorStep("confirmResetPassword")

/// A user has successfuly signed in but they have no verified attributes,
/// A user has successfully signed in but they have no verified attributes,
/// so they are presented with the Verify User view
public static let verifyUser = AuthenticatorStep("verifyUser")

/// A user has successfuly requested to verify an attribute and they need to provide a verification code,
/// A user has successfully requested to verify an attribute and they need to provide a verification code,
/// so they are presented with the Confirm Verify User view
public static let confirmVerifyUser = AuthenticatorStep("confirmVerifyUser")

/// An authenticated user has successfuly signed in.
/// An authenticated user has successfully signed in.
public static let signedIn = AuthenticatorStep("signedIn")
}

extension AuthenticatorInitialStep: Codable { }

extension AuthenticatorStep: Codable { }
12 changes: 12 additions & 0 deletions Sources/Authenticator/Models/Internal/AuthenticatorMFAType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

enum AuthenticatorMFAType {
case sms
case totp
case none
}
12 changes: 12 additions & 0 deletions Sources/Authenticator/Models/Internal/Step.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ enum Step {
case error(_ error: Error)
case signIn
case confirmSignInWithCustomChallenge
case confirmSignInWithTOTPCode
case continueSignInWithMFASelection(allowedMFATypes: AllowedMFATypes)
case continueSignInWithTOTPSetup(totpSetupDetails: TOTPSetupDetails)
case confirmSignInWithMFACode(deliveryDetails: AuthCodeDeliveryDetails?)
case confirmSignInWithNewPassword
case signUp
Expand Down Expand Up @@ -46,6 +49,12 @@ enum Step {
return .signIn
case .confirmSignInWithCustomChallenge:
return .confirmSignInWithCustomChallenge
case .confirmSignInWithTOTPCode:
return .confirmSignInWithTOTPCode
case .continueSignInWithTOTPSetup:
return .continueSignInWithTOTPSetup
case .continueSignInWithMFASelection:
return .continueSignInWithMFASelection
case .confirmSignInWithMFACode:
return .confirmSignInWithMFACode
case .confirmSignInWithNewPassword:
Expand Down Expand Up @@ -74,6 +83,9 @@ extension Step: Equatable {
case (.loading, .loading),
(.error, .error),
(.signIn, .signIn),
(.continueSignInWithMFASelection, .continueSignInWithMFASelection),
(.confirmSignInWithTOTPCode, .confirmSignInWithTOTPCode),
(.continueSignInWithTOTPSetup, .continueSignInWithTOTPSetup),
(.confirmSignInWithCustomChallenge, .confirmSignInWithCustomChallenge),
(.confirmSignInWithNewPassword, .confirmSignInWithNewPassword),
(.signUp, .signUp),
Expand Down
23 changes: 23 additions & 0 deletions Sources/Authenticator/Options/TOTPOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation

/// Options for configuring the TOTP MFA Experience
public struct TOTPOptions {

/// The `issuer` is the title displayed in a user's TOTP App preceding the
/// 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?

/// Creates a `TOTPOptions`
/// - Parameter issuer: The `issuer` is the title displayed in a user's TOTP App
public init(issuer: String? = nil) {
self.issuer = issuer
}
}
31 changes: 30 additions & 1 deletion Sources/Authenticator/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@

/* Confirm Sign In with Code */
"authenticator.confirmSignInWithCode.button.submit" = "Submit";
"authenticator.confirmSignInWithCode.button.backToSignIn" = "Back to Sign In";

/* Confirm Sign In with TOTP */
"authenticator.confirmSignInWithCode.totp.title" = "Enter your one-time passcode";
"authenticator.field.totp.code.label" = "Please enter the code from your registered Authenticator app";
"authenticator.field.totp.code.placeholder" = "Verification Code";
"authenticator.confirmSignInWithCode.totp.button.submit" = "Confirm";
"authenticator.confirmSignInWithCode.totp.button.backToSignIn" = "Back to Sign In";

/* Continue Sign In with MFA Selection */
"authenticator.continueSignInWithMFASelection.title" = "Select your preferred Two-Factor Auth method";
"authenticator.continueSignInWithMFASelection.sms.radioButton.title" = "Text Message (SMS)";
"authenticator.continueSignInWithMFASelection.totp.radioButton.title" = "Authenticator App (TOTP)";
"authenticator.continueSignInWithMFASelection.button.submit" = "Continue";
"authenticator.continueSignInWithMFASelection.button.backToSignIn" = "Back to Sign In";

/* Continue Sign In with TOTP Setup */
"authenticator.continueSignInWithTOTPSetup.title" = "Enable Two-Factor Auth";
"authenticator.continueSignInWithTOTPSetup.step1.label.title" = "Step 1: Download an Authenticator App";
"authenticator.continueSignInWithTOTPSetup.step1.label.content" = "Authenticator app generates one-time codes that can be used to verify your identity";
"authenticator.continueSignInWithTOTPSetup.step2.label.title" = "Step 2: Scan the QR code";
"authenticator.continueSignInWithTOTPSetup.step2.label.content" = "Open the Authenticator app and scan the QR code or enter the key to get your verification code";
"authenticator.continueSignInWithTOTPSetup.step3.label.title" = "Step 3: Verify your code";
"authenticator.continueSignInWithTOTPSetup.step3.label.content" = "Enter the 6 digit code from your Authenticator app";
"authenticator.continueSignInWithTOTPSetup.field.code.placeholder" = "Verification Code";
"authenticator.continueSignInWithTOTPSetup.button.copyKey" = "Copy Key";
"authenticator.continueSignInWithTOTPSetup.button.submit" = "Continue";
"authenticator.continueSignInWithTOTPSetup.button.backToSignIn" = "Back to Sign In";

/* Reset Password view */
"authenticator.resetPassword.title" = "Reset your password";
Expand Down Expand Up @@ -136,9 +164,10 @@

/* Authenticator Errors */
"authenticator.authError.incorrectCredentials" = "Incorrect username or password";
"authenticator.authError.continueSignInWithMFASelection.noSelectionError" = "Please select an MFA method to continue";
"authenticator.unknownError" = "Sorry, something went wrong";

"authenticator.cognitoError.codeDelivery" = "Could not send confirmation cde";
"authenticator.cognitoError.codeDelivery" = "Could not send confirmation code";
"authenticator.cognitoError.codeExpired" = "Confirmation code has expired";
"authenticator.cognitoError.codeMismatch" = "Incorrect confirmation code";
"authenticator.cognitoError.invalidPassword" = "The provided password is not valid";
Expand Down
Loading

0 comments on commit 6ec8a2f

Please sign in to comment.