Skip to content

Commit

Permalink
Added passkey authenticator to project
Browse files Browse the repository at this point in the history
  • Loading branch information
charliescheer committed Jul 16, 2024
1 parent 0dbf126 commit ce0f245
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Simplenote.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@
BA7575E82C46FAF9009C08FC /* PasskeyAuthChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7575E72C46FAF9009C08FC /* PasskeyAuthChallenge.swift */; };
BA7575EA2C46FB07009C08FC /* PasskeyAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7575E92C46FB07009C08FC /* PasskeyAuthResponse.swift */; };
BA7575EC2C46FB0B009C08FC /* PasskeyVerifyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7575EB2C46FB0B009C08FC /* PasskeyVerifyResponse.swift */; };
BA7575EE2C46FB30009C08FC /* PasskeyAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7575ED2C46FB30009C08FC /* PasskeyAuthenticator.swift */; };
BA7575F02C46FB80009C08FC /* Data+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7575EF2C46FB80009C08FC /* Data+Simplenote.swift */; };
BA78AF712B5B2BC300DCF896 /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA78AF702B5B2BC300DCF896 /* AutomatticTracks */; };
BA938CEE26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */; };
BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB642C0E46CC00B34EA7 /* Uploader.swift */; };
Expand Down Expand Up @@ -789,6 +791,8 @@
BA7575E72C46FAF9009C08FC /* PasskeyAuthChallenge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasskeyAuthChallenge.swift; sourceTree = "<group>"; };
BA7575E92C46FB07009C08FC /* PasskeyAuthResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasskeyAuthResponse.swift; sourceTree = "<group>"; };
BA7575EB2C46FB0B009C08FC /* PasskeyVerifyResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasskeyVerifyResponse.swift; sourceTree = "<group>"; };
BA7575ED2C46FB30009C08FC /* PasskeyAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasskeyAuthenticator.swift; sourceTree = "<group>"; };
BA7575EF2C46FB80009C08FC /* Data+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Simplenote.swift"; sourceTree = "<group>"; };
BA8CF21B2BFD20770087F33D /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
BA938CEB26ACFF4A00BE5A1D /* Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Remote.swift; sourceTree = "<group>"; };
BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountVerificationController+TestHelpers.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1261,6 +1265,7 @@
BAFB544F26CCA7F1006E037C /* NSProgressIndicator+Simplenote.swift */,
BA2C65CA26FE996100FA84E1 /* NSButton+Extensions.swift */,
BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */,
BA7575EF2C46FB80009C08FC /* Data+Simplenote.swift */,
);
name = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -1703,6 +1708,7 @@
BA6689622C45C51C009D4E84 /* Passkeys */ = {
isa = PBXGroup;
children = (
BA7575ED2C46FB30009C08FC /* PasskeyAuthenticator.swift */,
BA6689692C45C60E009D4E84 /* PasskeyRegistrator.swift */,
BA6689632C45C52B009D4E84 /* PasskeyRegistrationChallenge.swift */,
BA6689652C45C54B009D4E84 /* PasskeyRegistrationResponse.swift */,
Expand Down Expand Up @@ -2179,6 +2185,8 @@
B51AFE7725D36CDD00A196DF /* NSFont+Simplenote.swift in Sources */,
BA7575E62C45E269009C08FC /* BlockingActivityIndicator.swift in Sources */,
375D293D21E033D1007AB25A /* hash.c in Sources */,
BA7575F02C46FB80009C08FC /* Data+Simplenote.swift in Sources */,
BA7575EE2C46FB30009C08FC /* PasskeyAuthenticator.swift in Sources */,
B574CA74242D552100F8D02F /* TextViewInputHandler.swift in Sources */,
B5F807CD2481982B0048CBD7 /* Note+Simplenote.swift in Sources */,
A6C1E21525E010140076ADF7 /* SPApplication.swift in Sources */,
Expand Down
23 changes: 23 additions & 0 deletions Simplenote/Data+Simplenote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

extension Data {
/// Certain base 64 data values are encoded to be url safe. For the webauthn authentication we will need to decode the url safe data so that we can read it locally.
///
static func decodeUrlSafeBase64(_ value: String) throws -> Data {
var stringtoDecode: String = value.replacingOccurrences(of: "-", with: "+")
stringtoDecode = stringtoDecode.replacingOccurrences(of: "_", with: "/")
switch stringtoDecode.utf8.count % 4 {
case 2:
stringtoDecode += "=="
case 3:
stringtoDecode += "="
default:
break
}
guard let data = Data(base64Encoded: stringtoDecode, options: [.ignoreUnknownCharacters]) else {
throw NSError(domain: "decodeUrlSafeBase64", code: 1,
userInfo: [NSLocalizedDescriptionKey: "Can't decode base64 string"])
}
return data
}
}
77 changes: 77 additions & 0 deletions Simplenote/PasskeyAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation
import AuthenticationServices

class PasskeyAuthControllerDelegate: NSObject, ASAuthorizationControllerDelegate {

var onCompletion: ((Result<PasskeyAuthResponse, Error>) -> Void)?

public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
onCompletion?(.failure(error))
}

public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
onCompletion?(.failure(PasskeyError.authFailed))
return
}

let response = PasskeyAuthResponse(from: credential)
onCompletion?(.success(response))
}
}

class PasskeyAuthenticator: NSObject {
private let passkeyRemote: PasskeyRemote
private let internalAuthControllerDelegate: PasskeyAuthControllerDelegate

init(passkeyRemote: PasskeyRemote = PasskeyRemote(), authControllerDelegate: PasskeyAuthControllerDelegate = .init()) {
self.passkeyRemote = passkeyRemote
self.internalAuthControllerDelegate = authControllerDelegate
}

func attemptPasskeyAuth(for email: String, in presentationContext: PresentationContext) async throws -> PasskeyVerifyResponse {
let challenge = try await passkeyRemote.passkeyAuthChallenge(for: email)

let challengeData = try Data.decodeUrlSafeBase64(challenge.challenge)
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challenge.relayingParty)
let request = provider.createCredentialAssertionRequest(challenge: challengeData)

let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = internalAuthControllerDelegate
controller.presentationContextProvider = presentationContext

return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<PasskeyVerifyResponse, any Error>) in
internalAuthControllerDelegate.onCompletion = { [weak self] result in
guard let self else {
continuation.resume(throwing: PasskeyError.authFailed)
return
}

switch result {
case .success(let response):
Task {
do {
let verify = try await self.performPasskeyAuthentication(with: response)
continuation.resume(returning: verify)
} catch {
continuation.resume(throwing: error)
}
}

case .failure(let error):
continuation.resume(throwing: error)
}
}

controller.performRequests()
}
}

private func performPasskeyAuthentication(with response: PasskeyAuthResponse) async throws -> PasskeyVerifyResponse {
guard let response = try? await passkeyRemote.verifyPasskeyLogin(with: response) else {
throw PasskeyError.authFailed
}

return response
}
}
1 change: 1 addition & 0 deletions Simplenote/PasskeyTypeAliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import AuthenticationServices

typealias PresentationContext = ASAuthorizationControllerPresentationContextProviding
typealias PublicKeyCredentialRegistration = ASAuthorizationPlatformPublicKeyCredentialRegistration
typealias PublicKeyCredentialAssertion = ASAuthorizationPlatformPublicKeyCredentialAssertion

0 comments on commit ce0f245

Please sign in to comment.