Skip to content

Commit

Permalink
Add User Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
bwaresiak committed Feb 5, 2024
1 parent 930fce6 commit 9e5efbb
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 115 deletions.
8 changes: 8 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@
981FED76220464EF008488D7 /* AutoClearSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED75220464EF008488D7 /* AutoClearSettingsModel.swift */; };
9820EAF522613CD30089094D /* WebProgressWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9820EAF422613CD30089094D /* WebProgressWorker.swift */; };
9820FF502244FECC008D4782 /* UIScrollViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9820FF4F2244FECC008D4782 /* UIScrollViewExtension.swift */; };
9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */; };
982123502B6D233E00F08C57 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9821234F2B6D233E00F08C57 /* UserSession.swift */; };
9825F9DB293F2E8700F220F2 /* BookmarksTestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9825F9DA293F2E8700F220F2 /* BookmarksTestData.swift */; };
982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982686AC2600C0850011A8D6 /* ActionMessageView.swift */; };
982686B92600C0960011A8D6 /* ActionMessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982686B82600C0960011A8D6 /* ActionMessageView.xib */; };
Expand Down Expand Up @@ -1666,6 +1668,8 @@
9820A5D522B1C0B20024E37C /* DDG Trace.tracetemplate */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "DDG Trace.tracetemplate"; sourceTree = "<group>"; };
9820EAF422613CD30089094D /* WebProgressWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebProgressWorker.swift; sourceTree = "<group>"; };
9820FF4F2244FECC008D4782 /* UIScrollViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtension.swift; sourceTree = "<group>"; };
9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthenticator.swift; sourceTree = "<group>"; };
9821234F2B6D233E00F08C57 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
9825F9D7293F2DE900F220F2 /* PerformanceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PerformanceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
9825F9DA293F2E8700F220F2 /* BookmarksTestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTestData.swift; sourceTree = "<group>"; };
982686AC2600C0850011A8D6 /* ActionMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMessageView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5396,6 +5400,8 @@
983EABB7236198F6003948D1 /* DatabaseMigration.swift */,
853C5F6021C277C7001F7A05 /* global.swift */,
85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */,
9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */,
9821234F2B6D233E00F08C57 /* UserSession.swift */,
);
name = Application;
sourceTree = "<group>";
Expand Down Expand Up @@ -6808,12 +6814,14 @@
C1F341C52A6924000032057B /* EmailAddressPromptView.swift in Sources */,
316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */,
31C70B5B2804C61000FB6AD1 /* SaveAutofillLoginManager.swift in Sources */,
982123502B6D233E00F08C57 /* UserSession.swift in Sources */,
85449EFD23FDA71F00512AAF /* KeyboardSettings.swift in Sources */,
980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */,
4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */,
988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */,
850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */,
9817C9C321EF594700884F65 /* AutoClear.swift in Sources */,
9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */,
310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */,
85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */,
1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/AutofillLoginDetailsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class AutofillLoginDetailsViewController: UIViewController {
weak var delegate: AutofillLoginDetailsViewControllerDelegate?
private let viewModel: AutofillLoginDetailsViewModel
private var cancellables: Set<AnyCancellable> = []
private var authenticator = AutofillLoginListAuthenticator()
private var authenticator = AutofillLoginListAuthenticator(reason: UserText.autofillLoginListAuthenticationReason)
private let lockedView = AutofillItemsLockedView()
private let noAuthAvailableView = AutofillNoAuthAvailableView()
private var contentView: UIView?
Expand Down
66 changes: 7 additions & 59 deletions DuckDuckGo/AutofillLoginListAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,68 +22,16 @@ import Foundation
import LocalAuthentication
import Core

final class AutofillLoginListAuthenticator {
enum AuthError: Equatable {
case noAuthAvailable
case failedToAuthenticate
}

enum AuthenticationState {
case loggedIn, loggedOut, notAvailable
}

public struct Notifications {
public static let invalidateContext = Notification.Name("com.duckduckgo.app.AutofillLoginListAuthenticator.invalidateContext")
}

private var context = LAContext()
@Published private(set) var state = AuthenticationState.loggedOut

func logOut() {
state = .loggedOut
}
final class AutofillLoginListAuthenticator: UserAuthenticator {

func canAuthenticate() -> Bool {
var error: NSError?
let canAuthenticate = LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
return canAuthenticate
}
override func authenticate(completion: ((AuthError?) -> Void)? = nil) {

func authenticate(completion: ((AuthError?) -> Void)? = nil) {

if state == .loggedIn {
completion?(nil)
return
}

context = LAContext()
context.localizedCancelTitle = UserText.autofillLoginListAuthenticationCancelButton
let reason = UserText.autofillLoginListAuthenticationReason
context.localizedReason = reason

if canAuthenticate() {
let reason = reason
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in

DispatchQueue.main.async {
if success {
self.state = .loggedIn
completion?(nil)
} else {
os_log("Failed to authenticate: %s", log: .generalLog, type: .debug, error?.localizedDescription ?? "nil error")
AppDependencyProvider.shared.autofillLoginSession.endSession()
completion?(.failedToAuthenticate)
}
}
super.authenticate { error in
if error != nil {
AppDependencyProvider.shared.autofillLoginSession.endSession()
}
} else {
state = .notAvailable
AppDependencyProvider.shared.autofillLoginSession.endSession()
completion?(.noAuthAvailable)
}
}

func invalidateContext() {
context.invalidate()
completion?(error)
}
}
}
4 changes: 2 additions & 2 deletions DuckDuckGo/AutofillLoginListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ final class AutofillLoginListViewModel: ObservableObject {
case searchingNoResults
}

let authenticator = AutofillLoginListAuthenticator()
let authenticator = AutofillLoginListAuthenticator(reason: UserText.autofillLoginListAuthenticationReason)
var isSearching: Bool = false
var authenticationNotRequired = false
private var accounts = [SecureVaultModels.WebsiteAccount]()
Expand Down Expand Up @@ -104,7 +104,7 @@ final class AutofillLoginListViewModel: ObservableObject {
self.autofillNeverPromptWebsitesManager = autofillNeverPromptWebsitesManager

updateData()
authenticationNotRequired = !hasAccountsSaved || AppDependencyProvider.shared.autofillLoginSession.isValidSession
authenticationNotRequired = !hasAccountsSaved || AppDependencyProvider.shared.autofillLoginSession.isSessionValid
setupCancellables()
}

Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/AutofillLoginPromptViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ extension AutofillLoginPromptViewController: AutofillLoginPromptViewModelDelegat
Pixel.fire(pixel: .autofillLoginsFillLoginInlineManualConfirmed)
}

if AppDependencyProvider.shared.autofillLoginSession.isValidSession {
if AppDependencyProvider.shared.autofillLoginSession.isSessionValid {
dismiss(animated: true, completion: nil)
completion?(account, false)
return
Expand Down
29 changes: 4 additions & 25 deletions DuckDuckGo/AutofillLoginSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,21 @@
import Foundation
import BrowserServicesKit

class AutofillLoginSession {
final class AutofillLoginSession: UserSession {

private enum Constants {
static let timeout: TimeInterval = 15
}

private var sessionCreationDate: Date?
private var sessionAccount: SecureVaultModels.WebsiteAccount?
private let sessionTimeout: TimeInterval

init(sessionTimeout: TimeInterval = Constants.timeout) {
self.sessionTimeout = sessionTimeout
}

var isValidSession: Bool {
guard let sessionCreationDate = sessionCreationDate else { return false }
let timeInterval = Date().timeIntervalSince(sessionCreationDate)
// Check that timeInterval is > 0 to prevent a user circumventing by changing their device clock time
return timeInterval > 0 && timeInterval < sessionTimeout
}

var lastAccessedAccount: SecureVaultModels.WebsiteAccount? {
get {
return isValidSession ? sessionAccount : nil
return isSessionValid ? sessionAccount : nil
}
set {
sessionAccount = newValue
}
}

func startSession() {
sessionCreationDate = Date()
}

func endSession() {
sessionCreationDate = nil
override func endSession() {
super.endSession()
lastAccessedAccount = nil
}
}
19 changes: 14 additions & 5 deletions DuckDuckGo/SyncSettingsViewController+PDFRendering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ extension SyncSettingsViewController {

func shareRecoveryPDF() {

let data = RecoveryPDFGenerator()
.generate(recoveryCode)
if !userSession.isSessionValid {
userAuthenticator.logOut()
}

let pdf = RecoveryCodeItem(data: data)
navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf],
fromView: view)
userAuthenticator.authenticate { [weak self] error in
guard error == nil, let self else { return }

self.userSession.startSession()
let data = RecoveryPDFGenerator()
.generate(recoveryCode)

let pdf = RecoveryCodeItem(data: data)
navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf],
fromView: view)
}
}

func shareCode(_ code: String) {
Expand Down
76 changes: 64 additions & 12 deletions DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ import AVFoundation

extension SyncSettingsViewController: SyncManagementViewModelDelegate {

func authenticateUser() async -> Bool {
if !userSession.isSessionValid {
userAuthenticator.logOut()
}

return await withCheckedContinuation { continuation in
userAuthenticator.authenticate { [weak self] error in
guard error == nil, let self else {
continuation.resume(returning: false)
return
}
userSession.startSession()
continuation.resume(returning: true)
}
}
}

func launchAutofillViewController() {
guard let mainVC = view.window?.rootViewController as? MainViewController else { return }
dismiss(animated: true)
Expand Down Expand Up @@ -53,17 +70,27 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate {
}

func createAccountAndStartSyncing(optionsViewModel: SyncSettingsViewModel) {
Task { @MainActor in
do {
self.dismissPresentedViewController()
self.showPreparingSync()
try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType)
Pixel.fire(pixel: .syncSignupDirect, includedParameters: [.appVersion])
self.rootView.model.syncEnabled(recoveryCode: recoveryCode)
self.refreshDevices()
navigationController?.topViewController?.dismiss(animated: true, completion: showRecoveryPDF)
} catch {
handleError(SyncErrorMessage.unableToSyncToServer, error: error, event: .syncSignupError)

if !userSession.isSessionValid {
userAuthenticator.logOut()
}

userAuthenticator.authenticate { [weak self] error in
guard error == nil, let self else { return }
self.userSession.startSession()

Task { @MainActor in
do {
self.dismissPresentedViewController()
self.showPreparingSync()
try await self.syncService.createAccount(deviceName: self.deviceName, deviceType: self.deviceType)
Pixel.fire(pixel: .syncSignupDirect, includedParameters: [.appVersion])
self.rootView.model.syncEnabled(recoveryCode: self.recoveryCode)
self.refreshDevices()
self.navigationController?.topViewController?.dismiss(animated: true, completion: self.showRecoveryPDF)
} catch {
self.handleError(SyncErrorMessage.unableToSyncToServer, error: error, event: .syncSignupError)
}
}
}
}
Expand Down Expand Up @@ -103,10 +130,35 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate {
}

func showSyncWithAnotherDevice() {
collectCode(showConnectMode: true)
if !userSession.isSessionValid {
userAuthenticator.logOut()
}

userAuthenticator.authenticate { [weak self] error in
guard error == nil else {
return
}
self?.userSession.startSession()
self?.collectCode(showConnectMode: true)
}

}

func showRecoverData() {
if !userSession.isSessionValid {
userAuthenticator.logOut()
}

userAuthenticator.authenticate { [weak self] error in
guard error == nil else {
return
}

self?.userSession.startSession()
self?.dismissPresentedViewController()
self?.collectCode(showConnectMode: false)
}

dismissPresentedViewController()
collectCode(showConnectMode: false)
}
Expand Down
3 changes: 3 additions & 0 deletions DuckDuckGo/SyncSettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class SyncSettingsViewController: UIHostingController<SyncSettingsView> {
let syncBookmarksAdapter: SyncBookmarksAdapter
var connector: RemoteConnecting?

let userAuthenticator = UserAuthenticator(reason: UserText.syncUserUserAuthenticationReason)
let userSession = UserSession()

var recoveryCode: String {
guard let code = syncService.account?.recoveryCode else {
return ""
Expand Down
Loading

0 comments on commit 9e5efbb

Please sign in to comment.