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: Adding TOTP support #43

Merged
merged 8 commits into from
Nov 1, 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
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 { }
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