Skip to content

Commit 6ec8a2f

Browse files
authored
feat: Adding TOTP support (#43)
* 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
1 parent fd7e6f8 commit 6ec8a2f

File tree

58 files changed

+3005
-30
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3005
-30
lines changed

Sources/Authenticator/Authenticator.swift

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public struct Authenticator<LoadingContent: View,
1313
SignInContent: View,
1414
ConfirmSignInWithNewPasswordContent: View,
1515
ConfirmSignInWithMFACodeContent: View,
16+
ConfirmSignInWithTOTPCodeContent: View,
17+
ContinueSignInWithMFASelectionContent: View,
18+
ContinueSignInWithTOTPSetupContent: View,
1619
ConfirmSignInWithCustomChallengeContent: View,
1720
SignUpContent: View,
1821
ConfirmSignUpContent: View,
@@ -29,11 +32,15 @@ public struct Authenticator<LoadingContent: View,
2932
@State private var currentStep: Step = .loading
3033
@State private var previousStep: Step = .loading
3134
private var initialStep: AuthenticatorInitialStep
35+
private var totpOptions: TOTPOptions
3236
private var viewModifiers = ViewModifiers()
3337
private var contentStates: NSHashTable<AuthenticatorBaseState> = .weakObjects()
3438
private let loadingContent: LoadingContent
3539
private let signInContent: SignInContent
3640
private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent
41+
private let confirmSignInWithTOTPCodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent
42+
private let continueSignInWithMFASelectionContent: (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent
43+
private let continueSignInWithTOTPSetupContent: (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent
3744
private let confirmSignInContentWithCustomChallengeContent: ConfirmSignInWithCustomChallengeContent
3845
private let confirmSignInContentWithNewPasswordContent: ConfirmSignInWithNewPasswordContent
3946
private let signUpContent: SignUpContent
@@ -50,13 +57,21 @@ public struct Authenticator<LoadingContent: View,
5057
/// Creates an `Authenticator` component
5158
/// - Parameter initialStep: The initial step displayed to unauthorized users.
5259
/// Defaults to ``AuthenticatorInitialStep/signIn``
60+
/// - Parameter totpOptions: The TOTP Options that would be used by the Authenticator
61+
/// Defaults to ``.init()``
5362
/// - Parameter loadingContent: The content that is associated with the ``AuthenticatorStep/loading`` step.
5463
/// Defaults to a `SwiftUI.ProgressView`.
5564
/// - Parameter signInContent: The content associated with the ``AuthenticatorStep/signIn`` step.
5665
/// Defaults to a ``SignInView``.
57-
/// - Parameter confirmSignInWithMFACodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step.
66+
/// - Parameter confirmSignInWithMFACodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithMFACode`` step.
5867
/// Defaults to a ``ConfirmSignInWithMFACodeView``.
59-
/// - Parameter confirmSignInWithCustomChallengeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithMFACode`` step.
68+
///- Parameter confirmSignInWithTOTPCodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithTOTPCode`` step.
69+
/// Defaults to a ``ConfirmSignInWithMFACodeView``.
70+
///- Parameter continueSignInWithMFASelectionContent: The content associated with the ``AuthenticatorStep/continueSignInWithMFASelection`` step.
71+
/// Defaults to a ``ContinueSignInWithMFASelectionView``.
72+
///- Parameter continueSignInWithTOTPSetupContent: The content associated with the ``AuthenticatorStep/continueSignInWithTOTPSetup`` step.
73+
/// Defaults to a ``ContinueSignInWithTOTPSetupView``.
74+
/// - Parameter confirmSignInWithCustomChallengeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step.
6075
/// Defaults to a ``ConfirmSignInWithCustomChallengeView``.
6176
/// - Parameter confirmSignInWithNewPasswordContent: The content associated with the ``AuthenticatorStep/confirmSignInWithNewPassword`` step.
6277
/// Defaults to a ``ConfirmSignInWithNewPasswordView``.
@@ -81,6 +96,7 @@ public struct Authenticator<LoadingContent: View,
8196
/// - Parameter content: The content associated with the ``AuthenticatorStep/signedIn`` step, i.e. once the user has successfully authenticated.
8297
public init(
8398
initialStep: AuthenticatorInitialStep = .signIn,
99+
totpOptions: TOTPOptions = .init(),
84100
@ViewBuilder loadingContent: () -> LoadingContent = {
85101
ProgressView()
86102
},
@@ -90,6 +106,15 @@ public struct Authenticator<LoadingContent: View,
90106
@ViewBuilder confirmSignInWithMFACodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithMFACodeContent = { state in
91107
ConfirmSignInWithMFACodeView(state: state)
92108
},
109+
@ViewBuilder confirmSignInWithTOTPCodeContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent = { state in
110+
ConfirmSignInWithTOTPView(state: state)
111+
},
112+
@ViewBuilder continueSignInWithMFASelectionContent: @escaping (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent = { state in
113+
ContinueSignInWithMFASelectionView(state: state)
114+
},
115+
@ViewBuilder continueSignInWithTOTPSetupContent: @escaping (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent = { state in
116+
ContinueSignInWithTOTPSetupView(state: state)
117+
},
93118
@ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in
94119
ConfirmSignInWithCustomChallengeView(state: state)
95120
},
@@ -122,6 +147,7 @@ public struct Authenticator<LoadingContent: View,
122147
@ViewBuilder content: @escaping (SignedInState) -> SignedInContent
123148
) {
124149
self.initialStep = initialStep
150+
self.totpOptions = totpOptions
125151
self.loadingContent = loadingContent()
126152
let credentials = Credentials()
127153

@@ -135,6 +161,10 @@ public struct Authenticator<LoadingContent: View,
135161
confirmSignInWithMFACodeState
136162
)
137163

164+
self.confirmSignInWithTOTPCodeContent = confirmSignInWithTOTPCodeContent
165+
self.continueSignInWithMFASelectionContent = continueSignInWithMFASelectionContent
166+
self.continueSignInWithTOTPSetupContent = continueSignInWithTOTPSetupContent
167+
138168
let confirmSignInWithCustomChallengeState = ConfirmSignInWithCodeState(credentials: credentials)
139169
contentStates.add(confirmSignInWithMFACodeState)
140170
self.confirmSignInContentWithCustomChallengeContent = confirmSignInWithCustomChallengeContent(
@@ -302,6 +332,24 @@ public struct Authenticator<LoadingContent: View,
302332
confirmSignInContentWithNewPasswordContent
303333
case .confirmSignInWithMFACode:
304334
confirmSignInContentWithMFACodeContent
335+
case .continueSignInWithMFASelection(let allowedMFATypes):
336+
let continueSignInWithMFASelection = ContinueSignInWithMFASelectionState(
337+
authenticatorState: state,
338+
allowedMFATypes: allowedMFATypes
339+
)
340+
continueSignInWithMFASelectionContent(continueSignInWithMFASelection)
341+
case .confirmSignInWithTOTPCode:
342+
let confirmSignInWithCodeState = ConfirmSignInWithCodeState(
343+
authenticatorState: state
344+
)
345+
confirmSignInWithTOTPCodeContent(confirmSignInWithCodeState)
346+
case .continueSignInWithTOTPSetup(let totpSetupDetails):
347+
let totpStupState = ContinueSignInWithTOTPSetupState(
348+
authenticatorState: state,
349+
issuer: totpOptions.issuer,
350+
totpSetupDetails: totpSetupDetails
351+
)
352+
continueSignInWithTOTPSetupContent(totpStupState)
305353
case .confirmSignInWithCustomChallenge:
306354
confirmSignInContentWithCustomChallengeContent
307355
case .resetPassword:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
extension Bundle {
11+
12+
var applicationName: String? {
13+
if let localizedName = Bundle.main.infoDictionary?[kCFBundleLocalizationsKey as String] as? String {
14+
return localizedName
15+
}
16+
if let displayName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String {
17+
return displayName
18+
}
19+
if let bundleName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String {
20+
return bundleName
21+
}
22+
return nil
23+
}
24+
}

Sources/Authenticator/Models/AuthenticatorStep.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,27 @@ public struct AuthenticatorStep: Equatable {
4343
/// An unauthenticated user is presented with the Sing In view
4444
public static let signIn = AuthenticatorStep("signIn")
4545

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

50-
/// A user has successfuly provided valid Sign In credentials but is required to provide a MFA code,
50+
/// A user has successfully provided valid Sign In credentials but is required TOTP code from their associated authenticator token generator
51+
/// so they are presented with the Confirm Sign In with TOTP Code View
52+
public static let confirmSignInWithTOTPCode = AuthenticatorStep("confirmSignInWithTOTPCode")
53+
54+
/// A user has successfully provided valid Sign In credentials but is required to setup TOTP before continuing sign in
55+
/// so they are presented with the TOTP Setup View
56+
public static let continueSignInWithTOTPSetup = AuthenticatorStep("continueSignInWithTOTPSetup")
57+
58+
/// A user has successfully provided valid Sign In credentials but is required to select a MFA type to continue
59+
/// so they are presented with the Confirm Sign In with MFA Selection View
60+
public static let continueSignInWithMFASelection = AuthenticatorStep("continueSignInWithMFASelection")
61+
62+
/// A user has successfully provided valid Sign In credentials but is required to provide a MFA code,
5163
/// so they are presented with the Confirm Sign In with MFA Code view
5264
public static let confirmSignInWithMFACode = AuthenticatorStep("confirmSignInWithMFACode")
5365

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

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

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

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

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

80-
/// An authenticated user has successfuly signed in.
92+
/// An authenticated user has successfully signed in.
8193
public static let signedIn = AuthenticatorStep("signedIn")
8294
}
95+
96+
extension AuthenticatorInitialStep: Codable { }
97+
98+
extension AuthenticatorStep: Codable { }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
enum AuthenticatorMFAType {
9+
case sms
10+
case totp
11+
case none
12+
}

Sources/Authenticator/Models/Internal/Step.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ enum Step {
1313
case error(_ error: Error)
1414
case signIn
1515
case confirmSignInWithCustomChallenge
16+
case confirmSignInWithTOTPCode
17+
case continueSignInWithMFASelection(allowedMFATypes: AllowedMFATypes)
18+
case continueSignInWithTOTPSetup(totpSetupDetails: TOTPSetupDetails)
1619
case confirmSignInWithMFACode(deliveryDetails: AuthCodeDeliveryDetails?)
1720
case confirmSignInWithNewPassword
1821
case signUp
@@ -46,6 +49,12 @@ enum Step {
4649
return .signIn
4750
case .confirmSignInWithCustomChallenge:
4851
return .confirmSignInWithCustomChallenge
52+
case .confirmSignInWithTOTPCode:
53+
return .confirmSignInWithTOTPCode
54+
case .continueSignInWithTOTPSetup:
55+
return .continueSignInWithTOTPSetup
56+
case .continueSignInWithMFASelection:
57+
return .continueSignInWithMFASelection
4958
case .confirmSignInWithMFACode:
5059
return .confirmSignInWithMFACode
5160
case .confirmSignInWithNewPassword:
@@ -74,6 +83,9 @@ extension Step: Equatable {
7483
case (.loading, .loading),
7584
(.error, .error),
7685
(.signIn, .signIn),
86+
(.continueSignInWithMFASelection, .continueSignInWithMFASelection),
87+
(.confirmSignInWithTOTPCode, .confirmSignInWithTOTPCode),
88+
(.continueSignInWithTOTPSetup, .continueSignInWithTOTPSetup),
7789
(.confirmSignInWithCustomChallenge, .confirmSignInWithCustomChallenge),
7890
(.confirmSignInWithNewPassword, .confirmSignInWithNewPassword),
7991
(.signUp, .signUp),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
import Foundation
8+
9+
/// Options for configuring the TOTP MFA Experience
10+
public struct TOTPOptions {
11+
12+
/// The `issuer` is the title displayed in a user's TOTP App preceding the
13+
/// account name. In most cases, this should be the name of your app.
14+
/// For example, if your app is called "My App", your user will see
15+
/// "My App" - "username" in their TOTP app.
16+
public let issuer: String?
17+
18+
/// Creates a `TOTPOptions`
19+
/// - Parameter issuer: The `issuer` is the title displayed in a user's TOTP App
20+
public init(issuer: String? = nil) {
21+
self.issuer = issuer
22+
}
23+
}

Sources/Authenticator/Resources/en.lproj/Localizable.strings

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@
7676

7777
/* Confirm Sign In with Code */
7878
"authenticator.confirmSignInWithCode.button.submit" = "Submit";
79+
"authenticator.confirmSignInWithCode.button.backToSignIn" = "Back to Sign In";
80+
81+
/* Confirm Sign In with TOTP */
82+
"authenticator.confirmSignInWithCode.totp.title" = "Enter your one-time passcode";
83+
"authenticator.field.totp.code.label" = "Please enter the code from your registered Authenticator app";
84+
"authenticator.field.totp.code.placeholder" = "Verification Code";
85+
"authenticator.confirmSignInWithCode.totp.button.submit" = "Confirm";
86+
"authenticator.confirmSignInWithCode.totp.button.backToSignIn" = "Back to Sign In";
87+
88+
/* Continue Sign In with MFA Selection */
89+
"authenticator.continueSignInWithMFASelection.title" = "Select your preferred Two-Factor Auth method";
90+
"authenticator.continueSignInWithMFASelection.sms.radioButton.title" = "Text Message (SMS)";
91+
"authenticator.continueSignInWithMFASelection.totp.radioButton.title" = "Authenticator App (TOTP)";
92+
"authenticator.continueSignInWithMFASelection.button.submit" = "Continue";
93+
"authenticator.continueSignInWithMFASelection.button.backToSignIn" = "Back to Sign In";
94+
95+
/* Continue Sign In with TOTP Setup */
96+
"authenticator.continueSignInWithTOTPSetup.title" = "Enable Two-Factor Auth";
97+
"authenticator.continueSignInWithTOTPSetup.step1.label.title" = "Step 1: Download an Authenticator App";
98+
"authenticator.continueSignInWithTOTPSetup.step1.label.content" = "Authenticator app generates one-time codes that can be used to verify your identity";
99+
"authenticator.continueSignInWithTOTPSetup.step2.label.title" = "Step 2: Scan the QR code";
100+
"authenticator.continueSignInWithTOTPSetup.step2.label.content" = "Open the Authenticator app and scan the QR code or enter the key to get your verification code";
101+
"authenticator.continueSignInWithTOTPSetup.step3.label.title" = "Step 3: Verify your code";
102+
"authenticator.continueSignInWithTOTPSetup.step3.label.content" = "Enter the 6 digit code from your Authenticator app";
103+
"authenticator.continueSignInWithTOTPSetup.field.code.placeholder" = "Verification Code";
104+
"authenticator.continueSignInWithTOTPSetup.button.copyKey" = "Copy Key";
105+
"authenticator.continueSignInWithTOTPSetup.button.submit" = "Continue";
106+
"authenticator.continueSignInWithTOTPSetup.button.backToSignIn" = "Back to Sign In";
79107

80108
/* Reset Password view */
81109
"authenticator.resetPassword.title" = "Reset your password";
@@ -136,9 +164,10 @@
136164

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

141-
"authenticator.cognitoError.codeDelivery" = "Could not send confirmation cde";
170+
"authenticator.cognitoError.codeDelivery" = "Could not send confirmation code";
142171
"authenticator.cognitoError.codeExpired" = "Confirmation code has expired";
143172
"authenticator.cognitoError.codeMismatch" = "Incorrect confirmation code";
144173
"authenticator.cognitoError.invalidPassword" = "The provided password is not valid";

0 commit comments

Comments
 (0)