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

User Authentication #2431

Merged
merged 2 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
}
}
14 changes: 9 additions & 5 deletions DuckDuckGo/SyncSettingsViewController+PDFRendering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ extension SyncSettingsViewController {

func shareRecoveryPDF() {

let data = RecoveryPDFGenerator()
.generate(recoveryCode)
authenticateUser { [weak self] error in
guard error == nil, let self else { return }

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

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

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

extension SyncSettingsViewController: SyncManagementViewModelDelegate {

func authenticateUser() async -> Bool {
return await withCheckedContinuation { continuation in
authenticateUser { error in
if error == nil {
continuation.resume(returning: true)
} else {
continuation.resume(returning: false)
}
}
}
}

func launchAutofillViewController() {
guard let mainVC = view.window?.rootViewController as? MainViewController else { return }
dismiss(animated: true)
Expand Down Expand Up @@ -53,17 +65,20 @@ 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)
authenticateUser { [weak self] error in
guard error == nil, let self else { return }
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,12 +118,20 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate {
}

func showSyncWithAnotherDevice() {
collectCode(showConnectMode: true)
authenticateUser { [weak self] error in
guard error == nil, let self else { return }

self.collectCode(showConnectMode: true)
}
}

func showRecoverData() {
dismissPresentedViewController()
collectCode(showConnectMode: false)
authenticateUser { [weak self] error in
guard error == nil, let self else { return }

self.dismissPresentedViewController()
self.collectCode(showConnectMode: false)
}
}

func showDeviceConnected() {
Expand Down
16 changes: 16 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 Expand Up @@ -88,6 +91,19 @@ class SyncSettingsViewController: UIHostingController<SyncSettingsView> {
fatalError("init(coder:) has not been implemented")
}

func authenticateUser(completion: @escaping (UserAuthenticator.AuthError?) -> Void) {
if !userSession.isSessionValid {
userAuthenticator.logOut()
}

userAuthenticator.authenticate { [weak self] error in
if error == nil {
self?.userSession.startSession()
}
completion(error)
}
}

private func setUpSyncFeatureFlags(_ viewModel: SyncSettingsViewModel) {
syncService.featureFlagsPublisher.prepend(syncService.featureFlags)
.removeDuplicates()
Expand Down
Loading
Loading