From 58212ccf07e8e117f81936b2093b33c9b2c411bc Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Wed, 5 Jun 2024 12:03:52 -0600 Subject: [PATCH 01/27] Added basic setup to use ASAuthentication on the login form --- Simplenote/Classes/SPAuthViewController.swift | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index f6e27223e..890ba1e1b 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import SafariServices +import AuthenticationServices // MARK: - SPAuthViewController // @@ -188,6 +189,10 @@ class SPAuthViewController: UIViewController { passwordInputView.isHidden = mode.isPasswordHidden + if mode.isLogin { + displayAuthenticationOptions() + } + // hiding text from back button navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) } @@ -288,12 +293,17 @@ private extension SPAuthViewController { return } + if passwordInputView.isHidden == true && passwordInputView.text?.isEmpty == true { + passwordInputView.isHidden = false + return + } + if mustUpgradePasswordStrength() { performCredentialsValidation() return } - performSimperiumAuthentication() + performSimperiumAuthentication(username: email, password: password) } @IBAction func performSignUp() { @@ -357,10 +367,10 @@ private extension SPAuthViewController { } } - func performSimperiumAuthentication() { + func performSimperiumAuthentication(username: String, password: String) { lockdownInterface() - controller.loginWithCredentials(username: email, password: password) { error in + controller.loginWithCredentials(username: username, password: password) { error in if let error = error { self.handleError(error: error) } else { @@ -616,6 +626,38 @@ extension SPAuthViewController: SPTextInputViewDelegate { } } +// MARK: - ASAuthentication +// +extension SPAuthViewController: ASAuthorizationControllerDelegate { + private func displayAuthenticationOptions() { + let passwordRequest = ASAuthorizationPasswordProvider().createRequest() + let controller = ASAuthorizationController(authorizationRequests: [passwordRequest]) + controller.delegate = self + controller.presentationContextProvider = self + + controller.performRequests() + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let credential as ASPasswordCredential: + performSimperiumAuthentication(username: credential.user, password: credential.password) + default: + break + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { + passwordInputView.isHidden = false + } +} + +extension SPAuthViewController: ASAuthorizationControllerPresentationContextProviding { + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + view.window! + } +} + // MARK: - AuthenticationMode: Signup / Login // struct AuthenticationMode { @@ -627,6 +669,7 @@ struct AuthenticationMode { let secondaryActionText: String? let secondaryActionAttributedText: NSAttributedString? let isPasswordHidden: Bool + let isLogin: Bool } // MARK: - Default Operation Modes @@ -643,7 +686,8 @@ extension AuthenticationMode { secondaryActionSelector: #selector(SPAuthViewController.presentPasswordReset), secondaryActionText: AuthenticationStrings.loginSecondaryAction, secondaryActionAttributedText: nil, - isPasswordHidden: false) + isPasswordHidden: true, + isLogin: true) } /// Signup Operation Mode: Contains all of the strings + delegate wirings, so that the AuthUI handles user account creation scenarios. @@ -656,7 +700,8 @@ extension AuthenticationMode { secondaryActionSelector: #selector(SPAuthViewController.presentTermsOfService), secondaryActionText: nil, secondaryActionAttributedText: AuthenticationStrings.signupSecondaryAttributedAction, - isPasswordHidden: true) + isPasswordHidden: true, + isLogin: false) } } From a07083ed89fd9188bc3d18ccf54ad4af02570823 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Tue, 11 Jun 2024 12:31:34 -0600 Subject: [PATCH 02/27] Added request for passkey challenge to account remote --- Simplenote.xcodeproj/project.pbxproj | 6 +-- Simplenote/AccountRemote.swift | 41 +++++++++++++++ .../SPSettingsViewController+Extensions.swift | 50 +++++++++++++++++++ Simplenote/Classes/SPSettingsViewController.m | 16 ++++-- Simplenote/Classes/SimplenoteConstants.swift | 5 ++ Simplenote/Remote.swift | 13 +++++ 6 files changed, 125 insertions(+), 6 deletions(-) diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index c4d3df429..067259c48 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -509,13 +509,13 @@ BA8FC2A5267AC7470082962E /* SharedStorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FC2A4267AC7470082962E /* SharedStorageMigrator.swift */; }; BA9B19F926A8EF3200692366 /* SpinnerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B19F826A8EF3200692366 /* SpinnerViewController.swift */; }; BA9B59022685549F00DAD1ED /* StorageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B59012685549F00DAD1ED /* StorageSettings.swift */; }; - BA9C7EFB2BF2CC3E007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; - BA9C7EFC2BF2CCAE007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; BA9C7EC92BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EC82BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift */; }; BA9C7ECB2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECA2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift */; }; BA9C7ECD2BED813B007A8460 /* IntentTag+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECC2BED813B007A8460 /* IntentTag+Helpers.swift */; }; BA9C7ED02BEE9BA7007A8460 /* ShortcutIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECF2BEE9BA7007A8460 /* ShortcutIntents.intentdefinition */; }; BA9C7ED12BEE9BA7007A8460 /* ShortcutIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECF2BEE9BA7007A8460 /* ShortcutIntents.intentdefinition */; }; + BA9C7EFB2BF2CC3E007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; + BA9C7EFC2BF2CCAE007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; BAA4856925D5E40900F3BDB9 /* SearchQuery+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA4856825D5E40900F3BDB9 /* SearchQuery+Simplenote.swift */; }; BAA59E79269F9FE30068BD3D /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */; }; BAA63C3325EEDA83001589D7 /* NoteLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */; }; @@ -1199,7 +1199,6 @@ BA8FC2A4267AC7470082962E /* SharedStorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigrator.swift; sourceTree = ""; }; BA9B19F826A8EF3200692366 /* SpinnerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerViewController.swift; sourceTree = ""; }; BA9B59012685549F00DAD1ED /* StorageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettings.swift; sourceTree = ""; }; - BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BA9C7EC82BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyNoteContentIntentHandler.swift; sourceTree = ""; }; BA9C7ECA2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNoteWithTagIntentHandler.swift; sourceTree = ""; }; BA9C7ECC2BED813B007A8460 /* IntentTag+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentTag+Helpers.swift"; sourceTree = ""; }; @@ -1224,6 +1223,7 @@ BA9C7EF52BEE9C3D007A8460 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/ShortcutIntents.strings; sourceTree = ""; }; BA9C7EF72BEE9C3E007A8460 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/ShortcutIntents.strings"; sourceTree = ""; }; BA9C7EF92BEE9C3F007A8460 /* zh-Hans-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans-CN"; path = "zh-Hans-CN.lproj/ShortcutIntents.strings"; sourceTree = ""; }; + BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BAA4856825D5E40900F3BDB9 /* SearchQuery+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchQuery+Simplenote.swift"; sourceTree = ""; }; BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteLinkTests.swift; sourceTree = ""; }; diff --git a/Simplenote/AccountRemote.swift b/Simplenote/AccountRemote.swift index d91a16121..c35422f2d 100644 --- a/Simplenote/AccountRemote.swift +++ b/Simplenote/AccountRemote.swift @@ -51,4 +51,45 @@ class AccountRemote: Remote { return request } + + // MARK: - Passkeys + // + private func passkeyCredentialCreationRequest(withEmail email: String, password: String) -> URLRequest { + let params = [ + "email": email.lowercased(), + "password": password, + "webauthn": "true" + ] as [String: Any] + + let boundary = "Boundary-\(UUID().uuidString)" + var request = URLRequest(url: URL(string: "https://passkey-dev-dot-simple-note-hrd.appspot.com/api2/login")!, timeoutInterval: Double.infinity) + request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + request.httpMethod = "POST" + request.httpBody = body(with: boundary, parameters: params) + + return request + } + + private func body(with boundary: String, parameters: [String: Any]) -> Data { + var body = Data() + + for param in parameters { + let paramName = param.key + body += Data("--\(boundary)\r\n".utf8) + body += Data("Content-Disposition:form-data; name=\"\(paramName)\"".utf8) + let paramValue = param.value as! String + body += Data("\r\n\r\n\(paramValue)\r\n".utf8) + } + + body += Data("--\(boundary)--\r\n".utf8) + + return body + } + + func requestChallengeResponseToCreatePasskey(forEmail email: String, password: String) async throws -> Data? { + let request = passkeyCredentialCreationRequest(withEmail: email, password: password) + + return try await performDataTask(with: request) + } } diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index a07f8d4e9..8aae60a96 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -1,4 +1,5 @@ import UIKit +import AuthenticationServices // MARK: - Subscriber UI // @@ -218,6 +219,55 @@ extension SPSettingsViewController { } } +// MARK: - Passkeys +// +extension SPSettingsViewController { + @objc + func presentEnterPasswordAlert() { + let alert = UIAlertController(title: "Passkey Setup", message: "To add passkeys you must enter your password", preferredStyle: .alert) + alert.addTextField { textField in + textField.textContentType = .password + textField.isSecureTextEntry = true + } + + let action = UIAlertAction(title: "Submit", style: .default) { [unowned alert] _ in + guard let textfield = alert.textFields?.first, + let password = textfield.text, + let email = SPAppDelegate.shared().simperium.user?.email else { + return + } + + Task { + guard let response = try? await AccountRemote().requestChallengeResponseToCreatePasskey(forEmail: email, password: password), + let json = try? JSONSerialization.jsonObject(with: response, options: .topLevelDictionaryAssumed) as? [String: Any] else { + return + } + print(json) + } + } + alert.addAction(action) + alert.addCancelActionWithTitle("Cancel") + + present(alert, animated: true) + } +} + +extension SPSettingsViewController: ASAuthorizationControllerDelegate { + public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { + print(error.localizedDescription) + } + + public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + + } +} + +extension SPSettingsViewController: ASAuthorizationControllerPresentationContextProviding { + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + view.window! + } +} + private struct IndexAlert { static let title = NSLocalizedString("Index Removed", comment: "Alert title letting user know their search index has been removed") static let message = NSLocalizedString("Spotlight history may still appear in search results, but notes have be unindexed", comment: "Details that some results may still appear in searches on device") diff --git a/Simplenote/Classes/SPSettingsViewController.m b/Simplenote/Classes/SPSettingsViewController.m index ee90df625..d64715494 100644 --- a/Simplenote/Classes/SPSettingsViewController.m +++ b/Simplenote/Classes/SPSettingsViewController.m @@ -51,9 +51,10 @@ typedef NS_ENUM(NSInteger, SPOptionsViewSections) { typedef NS_ENUM(NSInteger, SPOptionsAccountRow) { SPOptionsAccountRowDescription = 0, - SPOptionsAccountRowPrivacy = 1, - SPOptionsAccountRowLogout = 2, - SPOptionsAccountRowCount = 3 + SPOptionsAccountRowPasskeys = 1, + SPOptionsAccountRowPrivacy = 2, + SPOptionsAccountRowLogout = 3, + SPOptionsAccountRowCount = 4 }; typedef NS_ENUM(NSInteger, SPOptionsNotesRow) { @@ -449,6 +450,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.selectionStyle = UITableViewCellSelectionStyleNone; break; } + case SPOptionsAccountRowPasskeys: { + cell.textLabel.text = NSLocalizedString(@"Add Passkey Authentication", @"Add passkeys to an account"); + cell.selectionStyle = UITableViewCellSelectionStyleNone; + break; + } case SPOptionsAccountRowPrivacy: { cell.textLabel.text = NSLocalizedString(@"Privacy Settings", @"Privacy Settings"); cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; @@ -577,6 +583,10 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } case SPOptionsViewSectionsAccount: { switch (indexPath.row) { + case SPOptionsAccountRowPasskeys: { + [self presentEnterPasswordAlert]; + break; + } case SPOptionsAccountRowPrivacy: { SPPrivacyViewController *test = [[SPPrivacyViewController alloc] initWithStyle:UITableViewStyleGrouped]; [self.navigationController pushViewController:test animated:true]; diff --git a/Simplenote/Classes/SimplenoteConstants.swift b/Simplenote/Classes/SimplenoteConstants.swift index f273e382e..5981b0cf5 100644 --- a/Simplenote/Classes/SimplenoteConstants.swift +++ b/Simplenote/Classes/SimplenoteConstants.swift @@ -44,4 +44,9 @@ class SimplenoteConstants: NSObject { static let signupURL = currentEngineBaseURL.appendingPathComponent("/account/request-signup") static let verificationURL = currentEngineBaseURL.appendingPathComponent("/account/verify-email/") static let accountDeletionURL = currentEngineBaseURL.appendingPathComponent("/account/request-delete/") + + /// Passkey: Endpoints + /// + static let currentPasskeyBaseURL = URL(string: "https://passkey-dev-dot-simple-note-hrd.appspot.com")! + static let passkeyCredentialCreationURL = currentPasskeyBaseURL.appendingPathComponent("/api2/login") } diff --git a/Simplenote/Remote.swift b/Simplenote/Remote.swift index 6b9424613..15415020a 100644 --- a/Simplenote/Remote.swift +++ b/Simplenote/Remote.swift @@ -30,4 +30,17 @@ class Remote { dataTask.resume() } + + func performDataTask(with request: URLRequest) async throws -> Data? { + try await withCheckedThrowingContinuation { continuation in + performDataTask(with: request) { result in + switch result { + case .success(let data): + continuation.resume(returning: data) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } } From b059b0a0876c9bf990aa693186dd9be5a5ede494 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Wed, 12 Jun 2024 11:06:11 -0600 Subject: [PATCH 03/27] Added ability to register passkeys on server --- Simplenote.xcodeproj/project.pbxproj | 8 ++++ Simplenote/AccountRemote.swift | 20 +++++++- .../SPSettingsViewController+Extensions.swift | 30 +++++++++++- Simplenote/Classes/SimplenoteConstants.swift | 1 + Simplenote/Classes/String+Simplenote.swift | 8 ++++ Simplenote/PasskeyChallenge.swift | 43 +++++++++++++++++ Simplenote/PasskeyRegistration.swift | 46 +++++++++++++++++++ .../Simplenote-Internal.entitlements | 1 + .../Supporting Files/Simplenote.entitlements | 1 + 9 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 Simplenote/PasskeyChallenge.swift create mode 100644 Simplenote/PasskeyRegistration.swift diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 067259c48..3648f3b68 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -420,6 +420,8 @@ BA0890A526BB9B680035CA48 /* NoteListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A426BB9B680035CA48 /* NoteListRow.swift */; }; BA0890A726BB9BE20035CA48 /* ListWidgetHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A626BB9BE20035CA48 /* ListWidgetHeaderView.swift */; }; BA0890A926BB9BF80035CA48 /* NewNoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A826BB9BF80035CA48 /* NewNoteButton.swift */; }; + BA0AC5442C1A0C65002964DB /* PasskeyRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0AC5432C1A0C65002964DB /* PasskeyRegistration.swift */; }; + BA0AC5462C1A0C71002964DB /* PasskeyChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0AC5452C1A0C71002964DB /* PasskeyChallenge.swift */; }; BA0AF10D2BE996600050EEBD /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59560DF251A46D500A06788 /* KeychainManager.swift */; }; BA0AF10E2BE996630050EEBD /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD30471FC4CFA2008D0B78 /* KeychainPasswordItem.swift */; }; BA0ED16E26D708AC002533B6 /* Color+Widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0ED16D26D708AC002533B6 /* Color+Widgets.swift */; }; @@ -1141,6 +1143,8 @@ BA0890A426BB9B680035CA48 /* NoteListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteListRow.swift; sourceTree = ""; }; BA0890A626BB9BE20035CA48 /* ListWidgetHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetHeaderView.swift; sourceTree = ""; }; BA0890A826BB9BF80035CA48 /* NewNoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteButton.swift; sourceTree = ""; }; + BA0AC5432C1A0C65002964DB /* PasskeyRegistration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyRegistration.swift; sourceTree = ""; }; + BA0AC5452C1A0C71002964DB /* PasskeyChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyChallenge.swift; sourceTree = ""; }; BA0ED16D26D708AC002533B6 /* Color+Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Widgets.swift"; sourceTree = ""; }; BA0F5E0326B62A8B0098C605 /* RemoteError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = ""; }; BA113E4C269E860500F3E3B4 /* markdown-light.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = "markdown-light.css"; sourceTree = ""; }; @@ -2415,6 +2419,8 @@ children = ( A6C7647F25E9131C00A39067 /* SignupRemote.swift */, A6CC0B0625B8287F00F12A85 /* AccountRemote.swift */, + BA0AC5432C1A0C65002964DB /* PasskeyRegistration.swift */, + BA0AC5452C1A0C71002964DB /* PasskeyChallenge.swift */, BA5768E2269A803F008B510E /* Remote.swift */, BA0F5E0326B62A8B0098C605 /* RemoteError.swift */, ); @@ -3453,6 +3459,7 @@ B56E763422BD394C00C5AA47 /* UIImage+Simplenote.swift in Sources */, B543C7E323CF76EA00003A80 /* NotesListFilter.swift in Sources */, 46A3C96717DFA81A002865AE /* NSManagedObjectContext+CoreDataExtensions.m in Sources */, + BA0AC5442C1A0C65002964DB /* PasskeyRegistration.swift in Sources */, A6C0DFA725C0992D00B9BE39 /* NoteScrollPositionCache.swift in Sources */, B59314D81A486B3800B651ED /* SPConstants.m in Sources */, 375D24B421E01131007AB25A /* escape.c in Sources */, @@ -3503,6 +3510,7 @@ A60DF31025A4524100FDADF3 /* PinLockBaseController.swift in Sources */, A6F487ED25A85F970050CFA8 /* TagTextFieldInputValidator.swift in Sources */, B5AB169822FA124F00B4EBA5 /* SPSheetController.swift in Sources */, + BA0AC5462C1A0C71002964DB /* PasskeyChallenge.swift in Sources */, B511AEDA255A0A2A005B2159 /* InterlinkResultsController.swift in Sources */, B5476BC123D8E5D0000E7723 /* String+Simplenote.swift in Sources */, B5DF734222A565DA00602CE7 /* Options.swift in Sources */, diff --git a/Simplenote/AccountRemote.swift b/Simplenote/AccountRemote.swift index c35422f2d..d9912873b 100644 --- a/Simplenote/AccountRemote.swift +++ b/Simplenote/AccountRemote.swift @@ -62,10 +62,10 @@ class AccountRemote: Remote { ] as [String: Any] let boundary = "Boundary-\(UUID().uuidString)" - var request = URLRequest(url: URL(string: "https://passkey-dev-dot-simple-note-hrd.appspot.com/api2/login")!, timeoutInterval: Double.infinity) + var request = URLRequest(url: SimplenoteConstants.passkeyCredentialCreationURL) request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" + request.httpMethod = RemoteConstants.Method.POST request.httpBody = body(with: boundary, parameters: params) return request @@ -92,4 +92,20 @@ class AccountRemote: Remote { return try await performDataTask(with: request) } + + func passkeyCredentialRegistration(withData data: Data) -> URLRequest { + var urlRequest = URLRequest(url: SimplenoteConstants.passkeyRegistrationURL) + urlRequest.httpMethod = RemoteConstants.Method.POST + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + urlRequest.httpBody = data + + return urlRequest + } + + func registerCredential(with data: Data) async throws { + let request = passkeyCredentialRegistration(withData: data) + try await _ = performDataTask(with: request) + } + } diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index 8aae60a96..e794da7d1 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -239,10 +239,11 @@ extension SPSettingsViewController { Task { guard let response = try? await AccountRemote().requestChallengeResponseToCreatePasskey(forEmail: email, password: password), - let json = try? JSONSerialization.jsonObject(with: response, options: .topLevelDictionaryAssumed) as? [String: Any] else { + let passkeyChallenge = try? JSONDecoder().decode(PasskeyChallenge.self, from: response) else { + //TODO: Throw instead return } - print(json) + self.attemptRegisteringPasskey(with: passkeyChallenge) } } alert.addAction(action) @@ -250,6 +251,20 @@ extension SPSettingsViewController { present(alert, animated: true) } + + private func attemptRegisteringPasskey(with passkeyChallenge: PasskeyChallenge) { + guard let challengeData = passkeyChallenge.challengeData, + let userID = passkeyChallenge.userID else { + return + } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: passkeyChallenge.relayingPartyIdentifier) + let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challengeData, name: passkeyChallenge.displayName, userID: userID) + let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest]) + authController.delegate = self + authController.presentationContextProvider = self + authController.performRequests() + } } extension SPSettingsViewController: ASAuthorizationControllerDelegate { @@ -259,6 +274,17 @@ extension SPSettingsViewController: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration { + guard let registrationObject = PasskeyRegistration(from: credential), + let data = try? JSONEncoder().encode(registrationObject) else { + //TODO: Should handle error + return + } + + Task { + try? await AccountRemote().registerCredential(with: data) + } + } } } diff --git a/Simplenote/Classes/SimplenoteConstants.swift b/Simplenote/Classes/SimplenoteConstants.swift index 5981b0cf5..e121388b5 100644 --- a/Simplenote/Classes/SimplenoteConstants.swift +++ b/Simplenote/Classes/SimplenoteConstants.swift @@ -49,4 +49,5 @@ class SimplenoteConstants: NSObject { /// static let currentPasskeyBaseURL = URL(string: "https://passkey-dev-dot-simple-note-hrd.appspot.com")! static let passkeyCredentialCreationURL = currentPasskeyBaseURL.appendingPathComponent("/api2/login") + static let passkeyRegistrationURL = currentPasskeyBaseURL.appendingPathComponent("/auth/add-credential") } diff --git a/Simplenote/Classes/String+Simplenote.swift b/Simplenote/Classes/String+Simplenote.swift index 750d31869..30f78606c 100644 --- a/Simplenote/Classes/String+Simplenote.swift +++ b/Simplenote/Classes/String+Simplenote.swift @@ -194,6 +194,14 @@ extension String { return base + host + "/" } + + func toBase64url() -> String { + let base64url = self + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + return base64url + } } // MARK: Replacing newlines with spaces diff --git a/Simplenote/PasskeyChallenge.swift b/Simplenote/PasskeyChallenge.swift new file mode 100644 index 000000000..79b17683b --- /dev/null +++ b/Simplenote/PasskeyChallenge.swift @@ -0,0 +1,43 @@ +import Foundation + +struct PasskeyChallenge: Decodable { + private struct User: Decodable { + let name: String + let userID: String + + enum CodingKeys: String, CodingKey { + case name + case userID = "id" + } + } + + private struct RelayingParty: Decodable { + let id: String + } + + private let relayingParty: PasskeyChallenge.RelayingParty + private let user: PasskeyChallenge.User + private let challenge: String + + enum CodingKeys: String, CodingKey { + case relayingParty = "rp" + case user + case challenge + } + + var relayingPartyIdentifier: String { + relayingParty.id + } + + var challengeData: Data? { + challenge.data(using: .utf8) + } + + var displayName: String { + user.name + } + + var userID: Data? { + user.userID.data(using: .utf8) + } +} diff --git a/Simplenote/PasskeyRegistration.swift b/Simplenote/PasskeyRegistration.swift new file mode 100644 index 000000000..70bdc70e5 --- /dev/null +++ b/Simplenote/PasskeyRegistration.swift @@ -0,0 +1,46 @@ +import Foundation +import AuthenticationServices + +struct PasskeyRegistration: Encodable { + struct Response: Encodable { + let clientDataJSON: String + let attestationObject: String + } + + private let email: String + private let id: String + private let rawId: String + private let type: String + private let response: PasskeyRegistration.Response + + init?(from credentialRegistration: ASAuthorizationPlatformPublicKeyCredentialRegistration) { + guard let email = SPAppDelegate.shared().simperium.user?.email, + let clientJson = Self.prepareJSON(from: credentialRegistration.rawClientDataJSON), + let rawAttestationObject = credentialRegistration.rawAttestationObject else { + return nil + } + + let idString = credentialRegistration.credentialID.base64EncodedString().toBase64url() + let response = Response(clientDataJSON: clientJson.base64EncodedString(), attestationObject: rawAttestationObject.base64EncodedString()) + + + self.email = email + self.id = idString + self.rawId = idString + self.type = "public-key" + self.response = response + } + + private static func prepareJSON(from data: Data) -> Data? { + guard var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + var base64challenge = json["challenge"] as? String, + var challengeData = Data(base64Encoded: base64challenge + "="), + var challenge = String(data: challengeData, encoding: .utf8) else { + return nil + } + + json["challenge"] = challenge + + return try? JSONSerialization.data(withJSONObject: json) + } +} diff --git a/Simplenote/Supporting Files/Simplenote-Internal.entitlements b/Simplenote/Supporting Files/Simplenote-Internal.entitlements index 523982a37..58b7e3a60 100644 --- a/Simplenote/Supporting Files/Simplenote-Internal.entitlements +++ b/Simplenote/Supporting Files/Simplenote-Internal.entitlements @@ -5,6 +5,7 @@ com.apple.developer.associated-domains webcredentials:simplenote.com + webcredentials:passkey-dev-dot-simple-note-hrd.appspot.com?mode=developer com.apple.security.application-groups diff --git a/Simplenote/Supporting Files/Simplenote.entitlements b/Simplenote/Supporting Files/Simplenote.entitlements index 523982a37..58b7e3a60 100644 --- a/Simplenote/Supporting Files/Simplenote.entitlements +++ b/Simplenote/Supporting Files/Simplenote.entitlements @@ -5,6 +5,7 @@ com.apple.developer.associated-domains webcredentials:simplenote.com + webcredentials:passkey-dev-dot-simple-note-hrd.appspot.com?mode=developer com.apple.security.application-groups From 6b6e22847046a2d4be8c4105d6d734fc14a1810e Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Wed, 12 Jun 2024 11:08:50 -0600 Subject: [PATCH 04/27] fixed a few linting errors --- Simplenote/PasskeyRegistration.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Simplenote/PasskeyRegistration.swift b/Simplenote/PasskeyRegistration.swift index 70bdc70e5..a122c9df3 100644 --- a/Simplenote/PasskeyRegistration.swift +++ b/Simplenote/PasskeyRegistration.swift @@ -23,7 +23,6 @@ struct PasskeyRegistration: Encodable { let idString = credentialRegistration.credentialID.base64EncodedString().toBase64url() let response = Response(clientDataJSON: clientJson.base64EncodedString(), attestationObject: rawAttestationObject.base64EncodedString()) - self.email = email self.id = idString self.rawId = idString @@ -33,9 +32,9 @@ struct PasskeyRegistration: Encodable { private static func prepareJSON(from data: Data) -> Data? { guard var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - var base64challenge = json["challenge"] as? String, - var challengeData = Data(base64Encoded: base64challenge + "="), - var challenge = String(data: challengeData, encoding: .utf8) else { + let base64challenge = json["challenge"] as? String, + let challengeData = Data(base64Encoded: base64challenge + "="), + let challenge = String(data: challengeData, encoding: .utf8) else { return nil } From 5363f4dbacddb0cd79901a9a08a973832707e7ac Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Wed, 12 Jun 2024 12:19:54 -0600 Subject: [PATCH 05/27] Added button for login with passkeys --- Simplenote/AccountRemote.swift | 21 +++++- Simplenote/Classes/SPAuthViewController.swift | 71 ++++++++++++++++--- Simplenote/Classes/SPAuthViewController.xib | 71 ++++++++++++++++--- Simplenote/Classes/SimplenoteConstants.swift | 2 + 4 files changed, 147 insertions(+), 18 deletions(-) diff --git a/Simplenote/AccountRemote.swift b/Simplenote/AccountRemote.swift index d9912873b..3ac0c52eb 100644 --- a/Simplenote/AccountRemote.swift +++ b/Simplenote/AccountRemote.swift @@ -93,7 +93,7 @@ class AccountRemote: Remote { return try await performDataTask(with: request) } - func passkeyCredentialRegistration(withData data: Data) -> URLRequest { + private func passkeyCredentialRegistration(withData data: Data) -> URLRequest { var urlRequest = URLRequest(url: SimplenoteConstants.passkeyRegistrationURL) urlRequest.httpMethod = RemoteConstants.Method.POST urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") @@ -108,4 +108,23 @@ class AccountRemote: Remote { try await _ = performDataTask(with: request) } + private func passkeyAuthChallengeRequest(forEmail email: String) -> URLRequest { + var urlRequest = URLRequest(url: SimplenoteConstants.passkeyAuthChallengeURL) + urlRequest.httpMethod = RemoteConstants.Method.POST + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body = [ + "email": email.lowercased() + ] + let json = try? JSONEncoder().encode(body) + + urlRequest.httpBody = json + + return urlRequest + } + + func passkeyAuthChallenge(for email: String) async throws -> Data? { + let request = passkeyAuthChallengeRequest(forEmail: email) + return try await performDataTask(with: request) + } } diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index 890ba1e1b..56b5199f5 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -76,6 +76,17 @@ class SPAuthViewController: UIViewController { } } + /// # Passkey Button Action + /// + @IBOutlet weak var passkeySigninButton: SPSquaredButton! { + didSet { + passkeySigninButton.isHidden = !mode.isLogin + passkeySigninButton.setTitle(AuthenticationStrings.passkeyActionButton, for: .normal) + passkeySigninButton.setTitleColor(.white, for: .normal) + passkeySigninButton.addTarget(self, action: #selector(passkeyAuthAction), for: .touchUpInside) + } + } + /// # Primary Action Spinner! /// @IBOutlet private var primaryActionSpinner: UIActivityIndicatorView! { @@ -103,6 +114,14 @@ class SPAuthViewController: UIViewController { } } + /// # Passkey Action Spinner + /// + @IBOutlet weak var passkeyActivitySpinner: UIActivityIndicatorView! { + didSet { + passkeyActivitySpinner.style = .medium + } + } + /// # Reveal Password Button /// private lazy var revealPasswordButton: UIButton = { @@ -187,11 +206,7 @@ class SPAuthViewController: UIViewController { setupNavigationController() startListeningToNotifications() - passwordInputView.isHidden = mode.isPasswordHidden - - if mode.isLogin { - displayAuthenticationOptions() - } + passwordInputView.isHidden = mode.isPasswordHidden\ // hiding text from back button navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) @@ -246,6 +261,7 @@ private extension SPAuthViewController { func ensureStylesMatchValidationState() { primaryActionButton.backgroundColor = isInputValid ? .simplenoteBlue50Color : .simplenoteGray20Color + passkeySigninButton.backgroundColor = isInputValid ? .simplenoteBlue50Color : .simplenoteGray20Color } @objc @@ -328,6 +344,14 @@ private extension SPAuthViewController { } } + @objc func passkeyAuthAction() { + Task { + //TODO: Handle errors + //TODO: Handle email not valid + try? await displayPasskeyAuthenticationOptions() + } + } + @IBAction func presentPasswordReset() { controller.presentPasswordReset(from: self, username: email) } @@ -629,15 +653,31 @@ extension SPAuthViewController: SPTextInputViewDelegate { // MARK: - ASAuthentication // extension SPAuthViewController: ASAuthorizationControllerDelegate { - private func displayAuthenticationOptions() { - let passwordRequest = ASAuthorizationPasswordProvider().createRequest() - let controller = ASAuthorizationController(authorizationRequests: [passwordRequest]) + private func displayPasskeyAuthenticationOptions() async throws { + guard let challenge = try await fetchAuthChallenge(), + let challengeData = challenge.challengeData else { + return + } + + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challenge.relayingParty) + let request = provider.createCredentialAssertionRequest(challenge: challengeData) + + let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } + private func fetchAuthChallenge() async throws -> PasskeyAuthChallenge? { + guard let data = try await AccountRemote().passkeyAuthChallenge(for: email) else { + return nil + } + let challenge = try JSONDecoder().decode(PasskeyAuthChallenge.self, from: data) + + return challenge + } + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential { case let credential as ASPasswordCredential: @@ -658,6 +698,20 @@ extension SPAuthViewController: ASAuthorizationControllerPresentationContextProv } } +struct PasskeyAuthChallenge: Decodable { + let relayingParty: String + let challenge: String + + enum CodingKeys: String, CodingKey { + case relayingParty = "rpId" + case challenge + } + + var challengeData: Data? { + challenge.data(using: .utf8) + } +} + // MARK: - AuthenticationMode: Signup / Login // struct AuthenticationMode { @@ -711,6 +765,7 @@ private enum AuthenticationStrings { static let loginTitle = NSLocalizedString("Log In", comment: "LogIn Interface Title") static let loginPrimaryAction = NSLocalizedString("Log In", comment: "LogIn Action") static let loginSecondaryAction = NSLocalizedString("Forgotten password?", comment: "Password Reset Action") + static let passkeyActionButton = NSLocalizedString("Log In With Passkeys", comment: "Login with Passkey action") static let signupTitle = NSLocalizedString("Sign Up", comment: "SignUp Interface Title") static let signupPrimaryAction = NSLocalizedString("Sign Up", comment: "SignUp Action") static let signupSecondaryActionPrefix = NSLocalizedString("By creating an account you agree to our", comment: "Terms of Service Legend *PREFIX*: printed in dark color") diff --git a/Simplenote/Classes/SPAuthViewController.xib b/Simplenote/Classes/SPAuthViewController.xib index ac1bcbb84..92a439671 100644 --- a/Simplenote/Classes/SPAuthViewController.xib +++ b/Simplenote/Classes/SPAuthViewController.xib @@ -1,10 +1,11 @@ - + - + + @@ -12,6 +13,8 @@ + + @@ -28,7 +31,7 @@ - + @@ -43,7 +46,7 @@ @@ -59,13 +62,13 @@ - + + + + + + + + + + + + + + - - - - - - - - - - - - - + - + + + + - + @@ -91,9 +110,10 @@ - + + @@ -108,7 +128,6 @@ - @@ -117,4 +136,20 @@ + + + + + + + + + + + + + + + + From 7e524e07e4db551c1c040fb13a3987ec650c4129 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 10:53:11 -0600 Subject: [PATCH 21/27] Updated release notes PR1599 added passkey authentication --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 680d9a9fb..0a72cceb3 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ - Extended support for iOS Shortcuts - Fixed issue where notes selection was lost when app backgrounded - Fixed an issue can activate the faceid switch without a passcode +- Added passkey authentication support 4.51 ----- From 4c72593ee053cbc237332ff99b05a2f1dfde15e1 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 15:26:40 -0600 Subject: [PATCH 22/27] Added feedback and errors for challenge registration of passkeys --- Simplenote.xcodeproj/project.pbxproj | 4 ++++ .../SPSettingsViewController+Extensions.swift | 16 +++++++++++++++- Simplenote/SPModalActivityIndicator.swift | 12 ++++++++++++ Simplenote/Simplenote-Bridging-Header.h | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Simplenote/SPModalActivityIndicator.swift diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index bb39735d1..5fa678f4e 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -439,6 +439,7 @@ BA12B06F26B0D0150026F31D /* SPManagedObject+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */; }; BA12B07026B0D0150026F31D /* SPManagedObject+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */; }; BA18532826488DBC00D9A347 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA18532726488DBC00D9A347 /* SignupRemoteTests.swift */; }; + BA1B70142C1CEC6F008282D7 /* SPModalActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */; }; BA2015BB2B57384F005E59AA /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA2015BA2B57384F005E59AA /* AutomatticTracks */; }; BA289B5C2BE4371A000E6794 /* ListWidgetIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA289B5A2BE4371A000E6794 /* ListWidgetIntentHandler.swift */; }; BA289B5F2BE43728000E6794 /* NoteWidgetIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA289B5D2BE43728000E6794 /* NoteWidgetIntentHandler.swift */; }; @@ -1158,6 +1159,7 @@ BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SPManagedObject+Widget.swift"; sourceTree = ""; }; BA16C6A82BC4968400C9079F /* Simplenote 7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Simplenote 7.xcdatamodel"; sourceTree = ""; }; BA18532726488DBC00D9A347 /* SignupRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupRemoteTests.swift; sourceTree = ""; }; + BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPModalActivityIndicator.swift; sourceTree = ""; }; BA289B5A2BE4371A000E6794 /* ListWidgetIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetIntentHandler.swift; sourceTree = ""; }; BA289B5D2BE43728000E6794 /* NoteWidgetIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteWidgetIntentHandler.swift; sourceTree = ""; }; BA289B632BE43963000E6794 /* OpenNewNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenNewNoteIntentHandler.swift; sourceTree = ""; }; @@ -2588,6 +2590,7 @@ B56A696422F9D54600B90398 /* SPLabel.swift */, E230E39C17D0F33B009B5EBB /* SPModalActivityIndicator.h */, E230E39D17D0F33B009B5EBB /* SPModalActivityIndicator.m */, + BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */, B5D982D122F8644000C37C29 /* SPSquaredButton.swift */, E215C79B180B228800AD36B5 /* SPTextField.h */, E215C79C180B228800AD36B5 /* SPTextField.m */, @@ -3599,6 +3602,7 @@ 375D24BA21E01131007AB25A /* document.c in Sources */, BA6DA19126DB5F1B000464C8 /* URLComponents.swift in Sources */, 46A3C98317DFA81A002865AE /* main.m in Sources */, + BA1B70142C1CEC6F008282D7 /* SPModalActivityIndicator.swift in Sources */, B58C9F5723F2FCEC008C3480 /* SPPlaceholderView.swift in Sources */, BAB6C04726BA4CAF007495C4 /* WidgetController.swift in Sources */, A6E1E79E24BDE401008A44BC /* SPCardTransitioningManager.swift in Sources */, diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index a73930d86..e76079921 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -232,6 +232,8 @@ extension SPSettingsViewController { let action = UIAlertAction(title: PasskeyAuthentication.submit, style: .default) { [unowned alert] _ in let appDelegate = SPAppDelegate.shared() + let activityIndicator = SPModalActivityIndicator.show(in: appDelegate.window) + guard let textfield = alert.textFields?.first, let password = textfield.text, let email = appDelegate.simperium.user?.email else { @@ -242,8 +244,10 @@ extension SPSettingsViewController { do { let authenticator = appDelegate.passkeyAuthenticator try await authenticator.registerPasskey(for: email, password: password, in: self) + await activityIndicator?.dismiss(true) } catch { - // TODO: Display some action for failure + await self.presentPasskeyRegistrationFailureAlert(activityIndicator: activityIndicator) + NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) } } } @@ -252,6 +256,13 @@ extension SPSettingsViewController { present(alert, animated: true) } + + private func presentPasskeyRegistrationFailureAlert(activityIndicator: SPModalActivityIndicator?) async { + await activityIndicator?.dismiss(true) + let failureAlert = UIAlertController(title: PasskeyAuthentication.failureTitle, message: PasskeyAuthentication.failureMessage, preferredStyle: .alert) + failureAlert.addCancelActionWithTitle(PasskeyAuthentication.okay) + self.present(failureAlert, animated: true) + } } extension SPSettingsViewController: ASAuthorizationControllerPresentationContextProviding { @@ -292,6 +303,9 @@ private struct PasskeyAuthentication { static let message = NSLocalizedString("To add passkeys you must enter your password", comment: "Message prompting user for password to create passkey") static let submit = NSLocalizedString("Submit", comment: "Submit button title") static let cancel = NSLocalizedString("cancel", comment: "Cancel button title") + static let failureTitle = NSLocalizedString("Passkey Registration Failed", comment: "Title for alert when passkey registration fails") + static let failureMessage = NSLocalizedString("Could not register passkey. Please try again later", comment: "Message for when passkey registration fails") + static let okay = NSLocalizedString("Okay", comment: "confirm button title") } // MARK: - RestorationAlert diff --git a/Simplenote/SPModalActivityIndicator.swift b/Simplenote/SPModalActivityIndicator.swift new file mode 100644 index 000000000..5500f65ef --- /dev/null +++ b/Simplenote/SPModalActivityIndicator.swift @@ -0,0 +1,12 @@ +import Foundation + +extension SPModalActivityIndicator { + // Swift is automatically creating an async version of this function for us, but then it is failing to build + // If you use the completion handler version of this method it complains. + // So you can't use the async version and you can't use the completion handler version... rock meet hard place + // This method is here to handle that issue + func dismiss(_ animated: Bool) async { + // Wrapping the dismiss in an anonymous closure silences the completion handler warning. + { dismiss(animated, completion: nil) }() + } +} diff --git a/Simplenote/Simplenote-Bridging-Header.h b/Simplenote/Simplenote-Bridging-Header.h index d433b86f9..ab0715d12 100644 --- a/Simplenote/Simplenote-Bridging-Header.h +++ b/Simplenote/Simplenote-Bridging-Header.h @@ -33,6 +33,7 @@ #import "SPTagEntryField.h" #import "WPAuthHandler.h" #import "NSManagedObjectContext+CoreDataExtensions.h" +#import "SPModalActivityIndicator.h" #pragma mark - Extensions From f67dcd1bc4ffeff16d4992ea0e8bb321c6a0a977 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 16:02:26 -0600 Subject: [PATCH 23/27] Added additional interaction/error handling for passkey registration --- .../SPSettingsViewController+Extensions.swift | 39 +++++++++++++++---- Simplenote/Classes/SPSettingsViewController.h | 7 +++- Simplenote/Classes/SPSettingsViewController.m | 1 + Simplenote/PasskeyAuthenticator.swift | 24 ++++++++++-- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index e76079921..1fee4ffe8 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -229,10 +229,11 @@ extension SPSettingsViewController { textField.textContentType = .password textField.isSecureTextEntry = true } + SPAppDelegate.shared().passkeyAuthenticator.registrationDelegate = self let action = UIAlertAction(title: PasskeyAuthentication.submit, style: .default) { [unowned alert] _ in let appDelegate = SPAppDelegate.shared() - let activityIndicator = SPModalActivityIndicator.show(in: appDelegate.window) + self.activityIndicator = SPModalActivityIndicator.show(in: appDelegate.window) guard let textfield = alert.textFields?.first, let password = textfield.text, @@ -244,10 +245,8 @@ extension SPSettingsViewController { do { let authenticator = appDelegate.passkeyAuthenticator try await authenticator.registerPasskey(for: email, password: password, in: self) - await activityIndicator?.dismiss(true) } catch { - await self.presentPasskeyRegistrationFailureAlert(activityIndicator: activityIndicator) - NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) + await self.passkeyRegistrationFailed(error) } } } @@ -257,20 +256,45 @@ extension SPSettingsViewController { present(alert, animated: true) } - private func presentPasskeyRegistrationFailureAlert(activityIndicator: SPModalActivityIndicator?) async { - await activityIndicator?.dismiss(true) + private func presentPasskeyRegistrationFailureAlert() { let failureAlert = UIAlertController(title: PasskeyAuthentication.failureTitle, message: PasskeyAuthentication.failureMessage, preferredStyle: .alert) failureAlert.addCancelActionWithTitle(PasskeyAuthentication.okay) self.present(failureAlert, animated: true) } } +// MARK: - Passkeys +// extension SPSettingsViewController: ASAuthorizationControllerPresentationContextProviding { + // To present the passkey modals we need to give it a window to appear in public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { view.window! } } +extension SPSettingsViewController: PasskeyRegistrationDelegate { + func passkeyRegistrationSucceed() async { + await removeActivityIndicator() + presentPasskeySuccessAlert() + } + + func passkeyRegistrationFailed(_ error: any Error) async { + await removeActivityIndicator() + self.presentPasskeyRegistrationFailureAlert() + NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) + } + + private func removeActivityIndicator() async { + await activityIndicator?.dismiss(true) + activityIndicator = nil + } + + private func presentPasskeySuccessAlert() { + let alert = UIAlertController(title: PasskeyAuthentication.registrationSuccessfullTitle, message: nil, preferredStyle: .alert) + alert.addCancelActionWithTitle(PasskeyAuthentication.okay) + } +} + private struct IndexAlert { static let title = NSLocalizedString("Index Removed", comment: "Alert title letting user know their search index has been removed") static let message = NSLocalizedString("Spotlight history may still appear in search results, but notes have be unindexed", comment: "Details that some results may still appear in searches on device") @@ -302,10 +326,11 @@ private struct PasskeyAuthentication { static let alertTitle = NSLocalizedString("Passkey Setup", comment: "Alert title for setting up passkeys") static let message = NSLocalizedString("To add passkeys you must enter your password", comment: "Message prompting user for password to create passkey") static let submit = NSLocalizedString("Submit", comment: "Submit button title") - static let cancel = NSLocalizedString("cancel", comment: "Cancel button title") + static let cancel = NSLocalizedString("Cancel", comment: "Cancel button title") static let failureTitle = NSLocalizedString("Passkey Registration Failed", comment: "Title for alert when passkey registration fails") static let failureMessage = NSLocalizedString("Could not register passkey. Please try again later", comment: "Message for when passkey registration fails") static let okay = NSLocalizedString("Okay", comment: "confirm button title") + static let registrationSuccessfullTitle = NSLocalizedString("Passkey Registration Successful", comment: "Alert title confirm passkey registration") } // MARK: - RestorationAlert diff --git a/Simplenote/Classes/SPSettingsViewController.h b/Simplenote/Classes/SPSettingsViewController.h index 301f929a0..65d3ce14b 100644 --- a/Simplenote/Classes/SPSettingsViewController.h +++ b/Simplenote/Classes/SPSettingsViewController.h @@ -1,5 +1,6 @@ #import #import "SPTableViewController.h" +#import "SPModalActivityIndicator.h" @interface SPSettingsViewController : SPTableViewController { //Preferences @@ -7,7 +8,9 @@ NSNumber *numPreviewLinesPref; } +@property (nonatomic, strong, nullable) SPModalActivityIndicator *activityIndicator; + @end -extern NSString *const SPAlphabeticalTagSortPref; -extern NSString *const SPSustainerAppIconName; +extern NSString * _Nonnull const SPAlphabeticalTagSortPref; +extern NSString * _Nonnull const SPSustainerAppIconName; diff --git a/Simplenote/Classes/SPSettingsViewController.m b/Simplenote/Classes/SPSettingsViewController.m index 8f3052d75..3d20dd2f0 100644 --- a/Simplenote/Classes/SPSettingsViewController.m +++ b/Simplenote/Classes/SPSettingsViewController.m @@ -20,6 +20,7 @@ @interface SPSettingsViewController () @property (nonatomic, strong) UIPickerView *pinTimeoutPickerView; @property (nonatomic, strong) UIToolbar *doneToolbar; @property (nonatomic, strong) UISwitch *indexNotesSwitch; + @end @implementation SPSettingsViewController { diff --git a/Simplenote/PasskeyAuthenticator.swift b/Simplenote/PasskeyAuthenticator.swift index 15ac64a0b..fb4359e57 100644 --- a/Simplenote/PasskeyAuthenticator.swift +++ b/Simplenote/PasskeyAuthenticator.swift @@ -3,6 +3,13 @@ import AuthenticationServices enum PasskeyError: Error { case couldNotRequestRegistrationChallenge + case couldNotEncodeRegistrationChallenge + case couldNotPrepareRegistrationData +} + +protocol PasskeyRegistrationDelegate: AnyObject { + func passkeyRegistrationSucceed() async + func passkeyRegistrationFailed(_ error: Error) async } @objcMembers @@ -11,6 +18,8 @@ class PasskeyAuthenticator: NSObject { let authenticator: SPAuthenticator let accountRemote: AccountRemote + weak var registrationDelegate: PasskeyRegistrationDelegate? = nil + @objc init(authenticator: SPAuthenticator) { self.authenticator = authenticator @@ -27,7 +36,7 @@ class PasskeyAuthenticator: NSObject { let passkeyChallenge = try JSONDecoder().decode(PasskeyRegistrationChallenge.self, from: data) attemptRegistration(with: passkeyChallenge, presentationContext: presentationContext) } catch { - throw PasskeyError.couldNotRequestRegistrationChallenge + throw PasskeyError.couldNotEncodeRegistrationChallenge } } @@ -47,7 +56,9 @@ class PasskeyAuthenticator: NSObject { private func performPasskeyRegistration(with credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) { guard let registrationObject = PasskeyRegistrationResponse(from: credential) else { - //TODO: Should handle error + Task { + await registrationDelegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) + } return } @@ -55,8 +66,9 @@ class PasskeyAuthenticator: NSObject { do { let data = try JSONEncoder().encode(registrationObject) try await accountRemote.registerCredential(with: data) + await registrationDelegate?.passkeyRegistrationSucceed() } catch { - //TODO: Display error + await registrationDelegate?.passkeyRegistrationFailed(error) } } } @@ -105,7 +117,11 @@ class PasskeyAuthenticator: NSObject { // extension PasskeyAuthenticator: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { - // TODO: handle error + if let registrationDelegate { + Task { + await registrationDelegate.passkeyRegistrationFailed(error) + } + } print(error.localizedDescription) } From 71887f1f2b5de9a99de4efe0231078b9d2afd7e2 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 16:10:57 -0600 Subject: [PATCH 24/27] Handle invalid email on passkey auth --- Simplenote/Classes/SPAuthViewController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index e4e25a2c8..176492b2b 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -320,9 +320,13 @@ private extension SPAuthViewController { } @objc func passkeyAuthAction() { + guard ensureWarningsAreOnScreenWhenNeeded() else { + return + } + Task { //TODO: Handle errors - //TODO: Handle email not valid + let passkeyAuthenticator = SPAppDelegate.shared().passkeyAuthenticator try? await passkeyAuthenticator.attemptPasskeyAuth(for: email, in: self) } From a1cbeb2b3db95c1464a49488c3db7cb435773fba Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 16:29:24 -0600 Subject: [PATCH 25/27] Added auth ui locking and release when using passkeys --- Simplenote/Classes/SPAuthViewController.swift | 22 +++++++++++++++++-- .../SPSettingsViewController+Extensions.swift | 8 +++---- Simplenote/PasskeyAuthenticator.swift | 18 ++++++++------- Simplenote/SPModalActivityIndicator.swift | 2 +- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index 176492b2b..d8ddc9c06 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -325,10 +325,16 @@ private extension SPAuthViewController { } Task { - //TODO: Handle errors + lockdownInterface() let passkeyAuthenticator = SPAppDelegate.shared().passkeyAuthenticator - try? await passkeyAuthenticator.attemptPasskeyAuth(for: email, in: self) + passkeyAuthenticator.delegate = self + do { + try await passkeyAuthenticator.attemptPasskeyAuth(for: email, in: self) + } catch { + unlockInterface() + //TODO: Show error message + } } } @@ -630,12 +636,24 @@ extension SPAuthViewController: SPTextInputViewDelegate { } } +// MARK: - Passkeys extension SPAuthViewController: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { view.window! } } +extension SPAuthViewController: PasskeyDelegate { + func passkeyRegistrationSucceed() async { + unlockInterface() + } + + func passkeyRegistrationFailed(_ error: any Error) async { + unlockInterface() + } + +} + // MARK: - AuthenticationMode: Signup / Login // struct AuthenticationMode { diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index 1fee4ffe8..11be483d8 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -229,7 +229,7 @@ extension SPSettingsViewController { textField.textContentType = .password textField.isSecureTextEntry = true } - SPAppDelegate.shared().passkeyAuthenticator.registrationDelegate = self + SPAppDelegate.shared().passkeyAuthenticator.delegate = self let action = UIAlertAction(title: PasskeyAuthentication.submit, style: .default) { [unowned alert] _ in let appDelegate = SPAppDelegate.shared() @@ -272,18 +272,18 @@ extension SPSettingsViewController: ASAuthorizationControllerPresentationContext } } -extension SPSettingsViewController: PasskeyRegistrationDelegate { +extension SPSettingsViewController: PasskeyDelegate { func passkeyRegistrationSucceed() async { await removeActivityIndicator() presentPasskeySuccessAlert() } - + func passkeyRegistrationFailed(_ error: any Error) async { await removeActivityIndicator() self.presentPasskeyRegistrationFailureAlert() NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) } - + private func removeActivityIndicator() async { await activityIndicator?.dismiss(true) activityIndicator = nil diff --git a/Simplenote/PasskeyAuthenticator.swift b/Simplenote/PasskeyAuthenticator.swift index fb4359e57..45f4fa760 100644 --- a/Simplenote/PasskeyAuthenticator.swift +++ b/Simplenote/PasskeyAuthenticator.swift @@ -5,9 +5,10 @@ enum PasskeyError: Error { case couldNotRequestRegistrationChallenge case couldNotEncodeRegistrationChallenge case couldNotPrepareRegistrationData + case authFailed } -protocol PasskeyRegistrationDelegate: AnyObject { +protocol PasskeyDelegate: AnyObject { func passkeyRegistrationSucceed() async func passkeyRegistrationFailed(_ error: Error) async } @@ -18,7 +19,7 @@ class PasskeyAuthenticator: NSObject { let authenticator: SPAuthenticator let accountRemote: AccountRemote - weak var registrationDelegate: PasskeyRegistrationDelegate? = nil + weak var delegate: PasskeyDelegate? = nil @objc init(authenticator: SPAuthenticator) { @@ -57,7 +58,7 @@ class PasskeyAuthenticator: NSObject { private func performPasskeyRegistration(with credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) { guard let registrationObject = PasskeyRegistrationResponse(from: credential) else { Task { - await registrationDelegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) + await delegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) } return } @@ -66,9 +67,9 @@ class PasskeyAuthenticator: NSObject { do { let data = try JSONEncoder().encode(registrationObject) try await accountRemote.registerCredential(with: data) - await registrationDelegate?.passkeyRegistrationSucceed() + await delegate?.passkeyRegistrationSucceed() } catch { - await registrationDelegate?.passkeyRegistrationFailed(error) + await delegate?.passkeyRegistrationFailed(error) } } } @@ -105,10 +106,12 @@ class PasskeyAuthenticator: NSObject { Task { @MainActor in guard let response = try? await accountRemote.verifyPasskeyLogin(with: json), let verifyResponse = try? JSONDecoder().decode(PasskeyVerifyResponse.self, from: response) else { + await self.delegate?.passkeyRegistrationFailed(PasskeyError.authFailed) return } authenticator.authenticate(withUsername: verifyResponse.username, token: verifyResponse.accessToken) + await self.delegate?.passkeyRegistrationSucceed() } } } @@ -117,12 +120,11 @@ class PasskeyAuthenticator: NSObject { // extension PasskeyAuthenticator: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { - if let registrationDelegate { + if let delegate { Task { - await registrationDelegate.passkeyRegistrationFailed(error) + await delegate.passkeyRegistrationFailed(error) } } - print(error.localizedDescription) } public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { diff --git a/Simplenote/SPModalActivityIndicator.swift b/Simplenote/SPModalActivityIndicator.swift index 5500f65ef..54a801529 100644 --- a/Simplenote/SPModalActivityIndicator.swift +++ b/Simplenote/SPModalActivityIndicator.swift @@ -5,7 +5,7 @@ extension SPModalActivityIndicator { // If you use the completion handler version of this method it complains. // So you can't use the async version and you can't use the completion handler version... rock meet hard place // This method is here to handle that issue - func dismiss(_ animated: Bool) async { + func dismiss(_ animated: Bool) { // Wrapping the dismiss in an anonymous closure silences the completion handler warning. { dismiss(animated, completion: nil) }() } From fdd994f4c227878c0d0ae02f2742efc5e5dda926 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 16:32:47 -0600 Subject: [PATCH 26/27] Removed async from auth delegate methods --- Simplenote/Classes/SPAuthViewController.swift | 4 ++-- .../SPSettingsViewController+Extensions.swift | 12 +++++------ Simplenote/PasskeyAuthenticator.swift | 20 ++++++++----------- Simplenote/SPModalActivityIndicator.swift | 7 +------ 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index d8ddc9c06..82f93197c 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -644,11 +644,11 @@ extension SPAuthViewController: ASAuthorizationControllerPresentationContextProv } extension SPAuthViewController: PasskeyDelegate { - func passkeyRegistrationSucceed() async { + func passkeyRegistrationSucceed() { unlockInterface() } - func passkeyRegistrationFailed(_ error: any Error) async { + func passkeyRegistrationFailed(_ error: any Error) { unlockInterface() } diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index 11be483d8..c2dc4ae02 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -273,19 +273,19 @@ extension SPSettingsViewController: ASAuthorizationControllerPresentationContext } extension SPSettingsViewController: PasskeyDelegate { - func passkeyRegistrationSucceed() async { - await removeActivityIndicator() + func passkeyRegistrationSucceed() { + removeActivityIndicator() presentPasskeySuccessAlert() } - func passkeyRegistrationFailed(_ error: any Error) async { - await removeActivityIndicator() + func passkeyRegistrationFailed(_ error: any Error) { + removeActivityIndicator() self.presentPasskeyRegistrationFailureAlert() NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) } - private func removeActivityIndicator() async { - await activityIndicator?.dismiss(true) + private func removeActivityIndicator() { + activityIndicator?.dismiss(true) activityIndicator = nil } diff --git a/Simplenote/PasskeyAuthenticator.swift b/Simplenote/PasskeyAuthenticator.swift index 45f4fa760..18ba1740b 100644 --- a/Simplenote/PasskeyAuthenticator.swift +++ b/Simplenote/PasskeyAuthenticator.swift @@ -9,8 +9,8 @@ enum PasskeyError: Error { } protocol PasskeyDelegate: AnyObject { - func passkeyRegistrationSucceed() async - func passkeyRegistrationFailed(_ error: Error) async + func passkeyRegistrationSucceed() + func passkeyRegistrationFailed(_ error: Error) } @objcMembers @@ -57,9 +57,7 @@ class PasskeyAuthenticator: NSObject { private func performPasskeyRegistration(with credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) { guard let registrationObject = PasskeyRegistrationResponse(from: credential) else { - Task { - await delegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) - } + delegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) return } @@ -67,9 +65,9 @@ class PasskeyAuthenticator: NSObject { do { let data = try JSONEncoder().encode(registrationObject) try await accountRemote.registerCredential(with: data) - await delegate?.passkeyRegistrationSucceed() + delegate?.passkeyRegistrationSucceed() } catch { - await delegate?.passkeyRegistrationFailed(error) + delegate?.passkeyRegistrationFailed(error) } } } @@ -106,12 +104,12 @@ class PasskeyAuthenticator: NSObject { Task { @MainActor in guard let response = try? await accountRemote.verifyPasskeyLogin(with: json), let verifyResponse = try? JSONDecoder().decode(PasskeyVerifyResponse.self, from: response) else { - await self.delegate?.passkeyRegistrationFailed(PasskeyError.authFailed) + self.delegate?.passkeyRegistrationFailed(PasskeyError.authFailed) return } authenticator.authenticate(withUsername: verifyResponse.username, token: verifyResponse.accessToken) - await self.delegate?.passkeyRegistrationSucceed() + self.delegate?.passkeyRegistrationSucceed() } } } @@ -121,9 +119,7 @@ class PasskeyAuthenticator: NSObject { extension PasskeyAuthenticator: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { if let delegate { - Task { - await delegate.passkeyRegistrationFailed(error) - } + delegate.passkeyRegistrationFailed(error) } } diff --git a/Simplenote/SPModalActivityIndicator.swift b/Simplenote/SPModalActivityIndicator.swift index 54a801529..68bd812a0 100644 --- a/Simplenote/SPModalActivityIndicator.swift +++ b/Simplenote/SPModalActivityIndicator.swift @@ -1,12 +1,7 @@ import Foundation extension SPModalActivityIndicator { - // Swift is automatically creating an async version of this function for us, but then it is failing to build - // If you use the completion handler version of this method it complains. - // So you can't use the async version and you can't use the completion handler version... rock meet hard place - // This method is here to handle that issue func dismiss(_ animated: Bool) { - // Wrapping the dismiss in an anonymous closure silences the completion handler warning. - { dismiss(animated, completion: nil) }() + dismiss(animated, completion: nil) } } From b0c9b8cbfc8c20301cba739471841e410d0ee61e Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 14 Jun 2024 16:40:43 -0600 Subject: [PATCH 27/27] Refactored handling passkey auth success and failure --- Simplenote/Classes/SPAuthViewController.swift | 14 ++++++++++---- .../SPSettingsViewController+Extensions.swift | 6 +++--- Simplenote/PasskeyAuthenticator.swift | 16 ++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index 82f93197c..46763f756 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -332,8 +332,7 @@ private extension SPAuthViewController { do { try await passkeyAuthenticator.attemptPasskeyAuth(for: email, in: self) } catch { - unlockInterface() - //TODO: Show error message + failed(error) } } } @@ -644,14 +643,20 @@ extension SPAuthViewController: ASAuthorizationControllerPresentationContextProv } extension SPAuthViewController: PasskeyDelegate { - func passkeyRegistrationSucceed() { + func succeed() { unlockInterface() } - func passkeyRegistrationFailed(_ error: any Error) { + func failed(_ error: any Error) { unlockInterface() + presentPasskeyAuthError(error) } + private func presentPasskeyAuthError(_ error: any Error) { + let alert = UIAlertController(title: AuthenticationStrings.passkeyAuthFailureTitle, message: error.localizedDescription, preferredStyle: .alert) + alert.addCancelActionWithTitle(AuthenticationStrings.unverifiedCancelText) + present(alert, animated: true) + } } // MARK: - AuthenticationMode: Signup / Login @@ -737,6 +742,7 @@ private enum AuthenticationStrings { static let unverifiedErrorMessage = NSLocalizedString("There was an preparing your verification email, please try again later", comment: "Request error alert message") static let verificationSentTitle = NSLocalizedString("Check your Email", comment: "Vefification sent alert title") static let verificationSentTemplate = NSLocalizedString("We’ve sent a verification email to %1$@. Please check your inbox and follow the instructions.", comment: "Confirmation that an email has been sent") + static let passkeyAuthFailureTitle = NSLocalizedString("Passkey Authentication Failed", comment: "Title for passkey authentication failure") } // MARK: - PasswordInsecure Alert Strings diff --git a/Simplenote/Classes/SPSettingsViewController+Extensions.swift b/Simplenote/Classes/SPSettingsViewController+Extensions.swift index c2dc4ae02..66ac2e5cc 100644 --- a/Simplenote/Classes/SPSettingsViewController+Extensions.swift +++ b/Simplenote/Classes/SPSettingsViewController+Extensions.swift @@ -246,7 +246,7 @@ extension SPSettingsViewController { let authenticator = appDelegate.passkeyAuthenticator try await authenticator.registerPasskey(for: email, password: password, in: self) } catch { - await self.passkeyRegistrationFailed(error) + self.failed(error) } } } @@ -273,12 +273,12 @@ extension SPSettingsViewController: ASAuthorizationControllerPresentationContext } extension SPSettingsViewController: PasskeyDelegate { - func passkeyRegistrationSucceed() { + func succeed() { removeActivityIndicator() presentPasskeySuccessAlert() } - func passkeyRegistrationFailed(_ error: any Error) { + func failed(_ error: any Error) { removeActivityIndicator() self.presentPasskeyRegistrationFailureAlert() NSLog("Failed to register Passkey. Error: %@", error.localizedDescription) diff --git a/Simplenote/PasskeyAuthenticator.swift b/Simplenote/PasskeyAuthenticator.swift index 18ba1740b..0b13105e5 100644 --- a/Simplenote/PasskeyAuthenticator.swift +++ b/Simplenote/PasskeyAuthenticator.swift @@ -9,8 +9,8 @@ enum PasskeyError: Error { } protocol PasskeyDelegate: AnyObject { - func passkeyRegistrationSucceed() - func passkeyRegistrationFailed(_ error: Error) + func succeed() + func failed(_ error: Error) } @objcMembers @@ -57,7 +57,7 @@ class PasskeyAuthenticator: NSObject { private func performPasskeyRegistration(with credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) { guard let registrationObject = PasskeyRegistrationResponse(from: credential) else { - delegate?.passkeyRegistrationFailed(PasskeyError.couldNotPrepareRegistrationData) + delegate?.failed(PasskeyError.couldNotPrepareRegistrationData) return } @@ -65,9 +65,9 @@ class PasskeyAuthenticator: NSObject { do { let data = try JSONEncoder().encode(registrationObject) try await accountRemote.registerCredential(with: data) - delegate?.passkeyRegistrationSucceed() + delegate?.succeed() } catch { - delegate?.passkeyRegistrationFailed(error) + delegate?.failed(error) } } } @@ -104,12 +104,12 @@ class PasskeyAuthenticator: NSObject { Task { @MainActor in guard let response = try? await accountRemote.verifyPasskeyLogin(with: json), let verifyResponse = try? JSONDecoder().decode(PasskeyVerifyResponse.self, from: response) else { - self.delegate?.passkeyRegistrationFailed(PasskeyError.authFailed) + self.delegate?.failed(PasskeyError.authFailed) return } authenticator.authenticate(withUsername: verifyResponse.username, token: verifyResponse.accessToken) - self.delegate?.passkeyRegistrationSucceed() + self.delegate?.succeed() } } } @@ -119,7 +119,7 @@ class PasskeyAuthenticator: NSObject { extension PasskeyAuthenticator: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { if let delegate { - delegate.passkeyRegistrationFailed(error) + delegate.failed(error) } }