-
Notifications
You must be signed in to change notification settings - Fork 84
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
macos authenticator #337
base: master
Are you sure you want to change the base?
macos authenticator #337
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,5 @@ webauthn-rs-demo-wasm/pkg | |
.DS_Store | ||
local | ||
build.sh | ||
.swiftpm | ||
.build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"pins" : [ | ||
{ | ||
"identity" : "swift-rs", | ||
"kind" : "remoteSourceControl", | ||
"location" : "https://github.com/Brendonovich/swift-rs", | ||
"state" : { | ||
"revision" : "a1578d5808b22b5a3461d36a6c8add5cd83a2ddf", | ||
"version" : "1.0.5" | ||
} | ||
} | ||
], | ||
"version" : 2 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// swift-tools-version: 5.8 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "MacAuthn", | ||
platforms: [ | ||
.macOS(.v12) | ||
], | ||
products: [ | ||
.library( | ||
name: "MacAuthn", | ||
type: .static, | ||
targets: ["MacAuthn"]), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.5") | ||
], | ||
targets: [ | ||
.target( | ||
name: "MacAuthn", | ||
dependencies: [ | ||
.product( | ||
name: "SwiftRs", | ||
package: "swift-rs" | ||
), | ||
]), | ||
] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# MacAuthn | ||
|
||
This package is built by `build.rs` using `webauthn-rs`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import AuthenticationServices | ||
import SwiftRs | ||
import Cocoa | ||
|
||
enum Result { | ||
case ok([String: Any]) | ||
case error(String) | ||
} | ||
|
||
class ApplicationDelegate: NSObject, NSApplicationDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { | ||
let window: NSWindow | ||
let authController: ASAuthorizationController | ||
var result: Result = .error("task did not finish") | ||
|
||
init(window: NSWindow, authController: ASAuthorizationController) { | ||
self.window = window | ||
self.authController = authController | ||
} | ||
|
||
func applicationDidFinishLaunching(_ notification: Notification) { | ||
authController.delegate = self | ||
authController.presentationContextProvider = self | ||
authController.performRequests() | ||
} | ||
|
||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { | ||
return window | ||
} | ||
|
||
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { | ||
if let credential = authorization.credential as? ASAuthorizationSecurityKeyPublicKeyCredentialRegistration { | ||
let rawId = credential.credentialID.toBase64Url() | ||
let clientDataJSON = credential.rawClientDataJSON.toBase64Url() | ||
let attestationObject = credential.rawAttestationObject!.toBase64Url() | ||
self.result = .ok([ | ||
"id": rawId, | ||
"rawId": rawId, | ||
"type": "public-key", | ||
"response": [ | ||
"clientDataJSON": clientDataJSON, | ||
"attestationObject": attestationObject | ||
] | ||
]) | ||
} else if let credential = authorization.credential as? ASAuthorizationSecurityKeyPublicKeyCredentialAssertion { | ||
let signature = credential.signature.toBase64Url() | ||
let clientDataJSON = credential.rawClientDataJSON.toBase64Url() | ||
let authenticatorData = credential.rawAuthenticatorData.toBase64Url() | ||
let rawId = credential.credentialID.toBase64Url() | ||
self.result = .ok([ | ||
"id": rawId, | ||
"rawId": rawId, | ||
"type": "public-key", | ||
"response": [ | ||
"clientDataJSON": clientDataJSON, | ||
"authenticatorData": authenticatorData, | ||
"signature": signature | ||
] | ||
]) | ||
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration { | ||
let rawId = credential.credentialID.toBase64Url() | ||
let clientDataJSON = credential.rawClientDataJSON.toBase64Url() | ||
let attestationObject = credential.rawAttestationObject!.toBase64Url() | ||
self.result = .ok([ | ||
"id": rawId, | ||
"rawId": rawId, | ||
"type": "public-key", | ||
"response": [ | ||
"clientDataJSON": clientDataJSON, | ||
"attestationObject": attestationObject | ||
] | ||
]) | ||
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion { | ||
let signature = credential.signature.toBase64Url() | ||
let clientDataJSON = credential.rawClientDataJSON.toBase64Url() | ||
let authenticatorData = credential.rawAuthenticatorData.toBase64Url() | ||
let rawId = credential.credentialID.toBase64Url() | ||
self.result = .ok([ | ||
"id": rawId, | ||
"rawId": rawId, | ||
"type": "public-key", | ||
"response": [ | ||
"clientDataJSON": clientDataJSON, | ||
"authenticatorData": authenticatorData, | ||
"signature": signature | ||
] | ||
]) | ||
} else { | ||
self.result = .error("unhandled credential") | ||
} | ||
NSApplication.shared.stop(0) | ||
} | ||
|
||
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { | ||
self.result = .error(error.localizedDescription) | ||
NSApplication.shared.stop(0) | ||
} | ||
} | ||
|
||
// ASAuthorizationController expects an ASPresentationAnchor (a NSWindow) | ||
// in order to know where to place itself. This function can create such | ||
// a window and run the NSRunLoop event loop to completion for it. | ||
func run(authController: ASAuthorizationController) -> String { | ||
NSApplication.shared.setActivationPolicy(.regular) | ||
let window = NSWindow(contentRect: NSMakeRect(0, 0, 1, 1), styleMask: .borderless, backing: .buffered, defer: false) | ||
window.center() | ||
window.makeKeyAndOrderFront(window) | ||
|
||
let applicationDelegate = ApplicationDelegate(window: window, authController: authController) | ||
NSApplication.shared.delegate = applicationDelegate | ||
|
||
NSApplication.shared.activate(ignoringOtherApps: true) | ||
NSApplication.shared.run() | ||
|
||
// Rust expects one of either {"data": ...} or {"error": ...} | ||
switch applicationDelegate.result { | ||
case let .ok(data): | ||
return String(data: try! JSONSerialization.data(withJSONObject: ["data": data]), encoding: .utf8)! | ||
case let .error(message): | ||
return String(data: try! JSONSerialization.data(withJSONObject: ["error": message]), encoding: .utf8)! | ||
} | ||
} | ||
|
||
@_cdecl("perform_register") | ||
public func performRegister(options: SRString) -> SRString { | ||
let options = try! JSONDecoder().decode(PublicKeyCredentialCreationOptions.self, from: Data(options.toArray())) | ||
|
||
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id) | ||
let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: options.challenge.decodeBase64Url()!, name: options.user.name, userID: options.user.id.decodeBase64Url()!) | ||
platformKeyRequest.displayName = options.user.displayName | ||
platformKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.authenticatorSelection.userVerification ?? "preferred") | ||
|
||
let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id) | ||
|
||
let securityKeyRequest = securityKeyProvider.createCredentialRegistrationRequest(challenge: options.challenge.decodeBase64Url()!, displayName: options.user.displayName, name: options.user.name, userID: options.user.id.decodeBase64Url()!) | ||
|
||
securityKeyRequest.credentialParameters = [] | ||
for publicKeyParam in options.pubKeyCredParams { | ||
let algorithm = ASCOSEAlgorithmIdentifier(rawValue: publicKeyParam.alg) | ||
let parameters = ASAuthorizationPublicKeyCredentialParameters(algorithm: algorithm) | ||
securityKeyRequest.credentialParameters.append(parameters) | ||
} | ||
|
||
securityKeyRequest.excludedCredentials = [] | ||
for credential in (options.excludeCredentials ?? []) { | ||
let id = credential.id.decodeBase64Url()! | ||
let transports = credential.transports?.map { | ||
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.init(rawValue: $0) | ||
} ?? ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported | ||
let credential = ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: id, transports: transports) | ||
securityKeyRequest.excludedCredentials.append(credential) | ||
} | ||
|
||
securityKeyRequest.attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind(rawValue: options.attestation ?? "none") | ||
securityKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.authenticatorSelection.userVerification ?? "preferred") | ||
|
||
if options.authenticatorSelection.requireResidentKey == true { | ||
securityKeyRequest.residentKeyPreference = .required | ||
} else { | ||
securityKeyRequest.residentKeyPreference = .preferred | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use the value of At the moment, this will always try to use resident keys if a key supports them, potentially filling the authenticator's limited storage with useless RKs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, this needs to default to discouraged. Because the current code here effectively is forcing "true" on all code paths. |
||
} | ||
|
||
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest, securityKeyRequest]) | ||
|
||
return SRString(run(authController: authController)) | ||
} | ||
|
||
@_cdecl("perform_auth") | ||
public func performAuth(options: SRString) -> SRString { | ||
let options = try! JSONDecoder().decode(PublicKeyCredentialRequestOptions.self, from: Data(options.toArray())) | ||
|
||
let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: options.rpId) | ||
let securityKeyRequest = securityKeyProvider.createCredentialAssertionRequest(challenge: options.challenge.decodeBase64Url()!) | ||
|
||
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rpId) | ||
let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: options.challenge.decodeBase64Url()!) | ||
|
||
securityKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.userVerification ?? "preferred") | ||
|
||
securityKeyRequest.allowedCredentials = [] | ||
for credential in (options.allowCredentials ?? []) { | ||
let id = credential.id.decodeBase64Url()! | ||
let transports = credential.transports?.map { | ||
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.init(rawValue: $0) | ||
} ?? ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported | ||
let descriptor = ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: id, transports: transports) | ||
securityKeyRequest.allowedCredentials.append(descriptor) | ||
} | ||
// Setting allowedCredentials can hang for some reason: https://developer.apple.com/forums/thread/727267 | ||
securityKeyRequest.allowedCredentials = [] | ||
|
||
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest, securityKeyRequest]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that |
||
|
||
return SRString(run(authController: authController)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by snek on 2023-08-05. | ||
// | ||
|
||
import Foundation | ||
|
||
struct PublicKeyCredentialDescriptor: Decodable { | ||
let id: String | ||
let transports: [String]? | ||
} | ||
|
||
struct PublicKeyCredentialUserEntity: Decodable { | ||
let id: String | ||
let name: String | ||
let displayName: String | ||
} | ||
|
||
struct PublicKeyCredentialRpEntity: Decodable { | ||
let id: String | ||
let name: String | ||
} | ||
|
||
struct PublicKeyCredentialParameters: Decodable { | ||
let type: String | ||
let alg: Int | ||
} | ||
|
||
struct AuthenticatorSelectionCriteria: Decodable { | ||
let userVerification: String? | ||
let requireResidentKey: Bool? | ||
} | ||
|
||
struct PublicKeyCredentialCreationOptions: Decodable { | ||
let rp: PublicKeyCredentialRpEntity | ||
let user: PublicKeyCredentialUserEntity | ||
let challenge: String | ||
let pubKeyCredParams: [PublicKeyCredentialParameters] | ||
let timeout: Double | ||
let excludeCredentials: [PublicKeyCredentialDescriptor]? | ||
let authenticatorSelection: AuthenticatorSelectionCriteria | ||
let attestation: String? | ||
} | ||
|
||
struct PublicKeyCredentialRequestOptions: Decodable { | ||
let challenge: String | ||
let timeout: Double | ||
let rpId: String | ||
let allowCredentials: [PublicKeyCredentialDescriptor]? | ||
let userVerification: String? | ||
} | ||
|
||
extension String { | ||
func decodeBase64Url() -> Data? { | ||
var base64 = self | ||
.replacingOccurrences(of: "-", with: "+") | ||
.replacingOccurrences(of: "_", with: "/") | ||
if base64.count % 4 != 0 { | ||
base64.append(String(repeating: "=", count: 4 - base64.count % 4)) | ||
} | ||
return Data(base64Encoded: base64) | ||
} | ||
} | ||
|
||
extension Data { | ||
func toBase64Url() -> String { | ||
return self | ||
.base64EncodedString() | ||
.replacingOccurrences(of: "+", with: "-") | ||
.replacingOccurrences(of: "/", with: "_") | ||
.replacingOccurrences(of: "=", with: "") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this section cause conflicts if there's already a running
NSApplication
for the process?