Skip to content

Commit

Permalink
Add domain support + implementation fixes (#11)
Browse files Browse the repository at this point in the history
* fix README

* Add init function to set domain

* README update

* improve implementation of passkey creation and getting in obj-c

* Add changeset

* example fix
  • Loading branch information
WyattMufson authored Aug 25, 2024
1 parent 0e0ff25 commit d9b644d
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-needles-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'electron-passkey': minor
---

A lot of cleanup and refactoring of the implementation
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ navigator.credentials.get = function (options) {
```js
import Passkey from 'electron-passkey';

Passkey.getInstance().init('domain.com');

ipcMain.handle('webauthn-create', (event, options) => {
return Passkey.getInstance().handlePasskeyCreate(options);
});
Expand All @@ -40,7 +42,7 @@ ipcMain.handle('webauthn-get', (event, options) => {
![AssociatedDomains](images/AssociatedDomains.png "Associated Domains")
3) You may need to create a provisioning profile for macOS development on your device and/or for distribution
4) Create a webserver to serve an AASA file [as specificed in the docs](https://developer.apple.com/documentation/xcode/supporting-associated-domains#Add-the-associated-domain-file-to-your-website)
5) Test it with the [yURL validator](https://branch.io/resources/aasa-validator/) and/or [branch.io validator](https://branch.io/resources/aasa-validator/)
5) Test it with the [yURL validator](https://yurl.chayev.com) and/or [branch.io validator](https://branch.io/resources/aasa-validator/)
6) Add the following to your entitlements plist
```
<key>com.apple.application-identifier</key>
Expand All @@ -52,7 +54,8 @@ ipcMain.handle('webauthn-get', (event, options) => {
</array>
```
7) Check to see if your AASA is being cached by the Apple CDN at `https://app-site-association.cdn-apple.com/a/v1/DOMAIN`
8) Build your electron application and sign it
8) Make sure to call `Passkey.getInstance().init()` and pass in your domain
9) Build your electron application and sign it

### Deployments

Expand Down
2 changes: 2 additions & 0 deletions src/demo/electron-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import './ipcHandlers';
// https://github.com/electron/electron/issues/25153
// app.disableHardwareAcceleration();

Passkey.getInstance().init('google.com');

let window: BrowserWindow;

function createWindow() {
Expand Down
73 changes: 65 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ interface PasskeyOptions {
signal?: AbortSignal;
}

interface PasskeyHandler {
HandlePasskeyCreate(options: string): Promise<string>;
HandlePasskeyGet(options: string): Promise<string>;
}

interface PasskeyInterface {
HandlePasskeyCreate: (options: string) => Promise<string>;
HandlePasskeyGet: (options: string) => Promise<string>;
PasskeyHandler: any;
PasskeyHandler: new () => PasskeyHandler;
}

const lib: PasskeyInterface = require('node-gyp-build')(join(__dirname, '..'));
Expand All @@ -68,14 +71,26 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}

function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i += 1) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

class Passkey {
// eslint-disable-next-line
private static instance: Passkey;

private handler: any;
private handler: PasskeyHandler;

private platform = os.platform();

private domain: string = '';

private constructor() {
this.handler = new lib.PasskeyHandler(); // Create an instance of PasskeyHandler
}
Expand All @@ -88,7 +103,13 @@ class Passkey {
return Passkey.instance;
}

handlePasskeyCreate(options: PasskeyOptions): Promise<string> {
init(domain: string): void {
this.domain = domain;
}

async handlePasskeyCreate(
options: PasskeyOptions,
): Promise<PublicKeyCredential> {
if (this.platform !== 'darwin') {
throw new Error(
`electron-passkey is meant for macOS only and should NOT be run on ${this.platform}`,
Expand All @@ -97,22 +118,58 @@ class Passkey {
options.publicKey.challenge = arrayBufferToBase64(
options.publicKey.challenge as ArrayBuffer,
);
(options.publicKey as PublicKeyCredentialCreationOptions).rp.id =
this.domain;
(options.publicKey as PublicKeyCredentialCreationOptions).user.id =
arrayBufferToBase64(
(options.publicKey as PublicKeyCredentialCreationOptions).user
.id as ArrayBuffer,
);

return this.handler.HandlePasskeyCreate(JSON.stringify(options));
const rawString = await this.handler.HandlePasskeyCreate(
JSON.stringify(options),
);
let raw;
try {
raw = JSON.parse(rawString);
} catch (e: any) {
throw new Error(`Failed to parse JSON response: ${e.message}`);
}

try {
raw.rawId = base64ToArrayBuffer(raw.rawId);
} catch (e: any) {
throw new Error(`Failed to convert rawId from base64: ${e.message}`);
}
return raw;
}

handlePasskeyGet(options: PasskeyOptions): Promise<string> {
async handlePasskeyGet(
options: PasskeyOptions,
): Promise<PublicKeyCredential> {
if (this.platform !== 'darwin') {
throw new Error(
`electron-passkey is meant for macOS only and should NOT be run on ${this.platform}`,
);
}
return this.handler.HandlePasskeyGet(JSON.stringify(options));
(options.publicKey as PublicKeyCredentialRequestOptions).rpId = this.domain;

const rawString = await this.handler.HandlePasskeyGet(
JSON.stringify(options),
);
let raw;
try {
raw = JSON.parse(rawString);
} catch (e: any) {
throw new Error(`Failed to parse JSON response: ${e.message}`);
}

try {
raw.rawId = base64ToArrayBuffer(raw.rawId);
} catch (e: any) {
throw new Error(`Failed to convert rawId from base64: ${e.message}`);
}
return raw;
}

static getPackageName(): string {
Expand Down
54 changes: 50 additions & 4 deletions src/lib/passkey.mm
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,57 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl
NSString *credentialId = [credential.credentialID base64EncodedStringWithOptions:0];

NSDictionary *responseDict = @{
@"clientDataJSON": [clientDataJSON base64EncodedStringWithOptions:0],
@"attestationObject": [attestationObject base64EncodedStringWithOptions:0],
@"credentialId": credentialId
@"clientDataJSON": [credential.rawClientDataJSON base64EncodedStringWithOptions:0],
@"attestationObject": [credential.rawAttestationObject base64EncodedStringWithOptions:0]
};

// Assemble the PublicKeyCredential object structure
NSDictionary *publicKeyCredentialDict = @{
@"id": [credential.credentialID base64EncodedStringWithOptions:0], // id is the base64-encoded credential ID
@"type": @"public-key", // Fixed value for PublicKeyCredential
@"rawId": [credential.credentialID base64EncodedStringWithOptions:0], // rawId is the raw NSData representing the credential ID
@"response": responseDict, // The response object
@"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example
@"transports": @[] // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialRegistration
};

NSError *error;
NSData *responseData = [NSJSONSerialization dataWithJSONObject:publicKeyCredentialDict options:0 error:&error];
if (error) {
NSLog(@"[authorizationController didCompleteWithAuthorization]: Failed to serialize response: %@", error.localizedDescription);
if (self.completionHandler) {
self.completionHandler(nil, error.localizedDescription);
}
} else {
NSString *resultMessage = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
if (self.completionHandler) {
self.completionHandler(resultMessage, nil);
}
}
} else if ([authorization.credential isKindOfClass:[ASAuthorizationPlatformPublicKeyCredentialAssertion class]]) {
ASAuthorizationPlatformPublicKeyCredentialAssertion *credential = (ASAuthorizationPlatformPublicKeyCredentialAssertion *)authorization.credential;

// Create the "response" dictionary, simulating the AuthenticatorAssertionResponse
NSDictionary *responseDict = @{
@"clientDataJSON": [credential.rawClientDataJSON base64EncodedStringWithOptions:0],
@"authenticatorData": [credential.rawAuthenticatorData base64EncodedStringWithOptions:0],
@"signature": [credential.signature base64EncodedStringWithOptions:0],
@"userHandle": credential.userID ? [credential.userID base64EncodedStringWithOptions:0] : [NSNull null]
};

// Assemble the PublicKeyCredential object structure
NSDictionary *publicKeyCredentialDict = @{
@"id": [credential.credentialID base64EncodedStringWithOptions:0], // id is the base64-encoded credential ID
@"type": @"public-key", // Fixed value for PublicKeyCredential
@"rawId": [credential.credentialID base64EncodedStringWithOptions:0], // rawId is the base64-encoded credential ID
@"response": responseDict, // The response object
@"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example
@"transports": @[] // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion
};

// Serialize the PublicKeyCredential object into JSON
NSError *error;
NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseDict options:0 error:&error];
NSData *responseData = [NSJSONSerialization dataWithJSONObject:publicKeyCredentialDict options:0 error:&error];
if (error) {
NSLog(@"[authorizationController didCompleteWithAuthorization]: Failed to serialize response: %@", error.localizedDescription);
if (self.completionHandler) {
Expand Down Expand Up @@ -282,6 +326,8 @@ - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthoriz
deferred.Resolve(Napi::String::New(env, std::string([resultMessage UTF8String])));
}
}];

return deferred.Promise();
}

private:
Expand Down

0 comments on commit d9b644d

Please sign in to comment.