Skip to content

Commit

Permalink
Passkeys (#28)
Browse files Browse the repository at this point in the history
* wip: passkeys

* Make `userName` optional

* Add `useBrowserAutofill` option

* Add overloads to restrict passing a token and autofill

* Pass empty body to fix 401

* Throw error if both a token and autofill is specified
  • Loading branch information
hwhmeikle committed May 16, 2023
1 parent d7618f2 commit a48a5e6
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 6 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@authsignal/browser",
"version": "0.0.22",
"version": "0.1.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
Expand All @@ -21,13 +21,16 @@
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^3.3.6",
"@simplewebauthn/browser": "^7.2.0",
"a11y-dialog": "^7.5.2",
"iframe-resizer": "^4.3.6",
"ky": "^0.33.3",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-typescript": "^8.5.0",
"@simplewebauthn/typescript-types": "^7.0.0",
"@types/iframe-resizer": "^3.5.9",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.39.0",
Expand Down
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./passkey-api-client";
78 changes: 78 additions & 0 deletions src/api/passkey-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import ky from "ky";
import {KyInstance} from "ky/distribution/types/ky";
import {
AddAuthenticatorRequest,
AddAuthenticatorResponse,
AuthenticationOptsRequest,
AuthenticationOptsResponse,
RegistrationOptsRequest,
RegistrationOptsResponse,
VerifyRequest,
VerifyResponse,
} from "./types";

type PasskeyApiClientOptions = {
baseUrl: string;
tenantId: string;
};

export class PasskeyApiClient {
tenantId: string;
api: KyInstance;

constructor({baseUrl, tenantId}: PasskeyApiClientOptions) {
this.tenantId = tenantId;

this.api = ky.create({
prefixUrl: baseUrl,
});
}

async registrationOptions({token, userName}: RegistrationOptsRequest): Promise<RegistrationOptsResponse> {
const response = await this.api.post("user-authenticators/passkey/registration-options", {
json: {userName},
headers: {
Authorization: `Bearer ${token}`,
},
});

return response.json();
}

async authenticationOptions({token}: AuthenticationOptsRequest): Promise<AuthenticationOptsResponse> {
const authorizationHeader = token ? `Bearer ${token}` : `Basic ${Buffer.from(this.tenantId).toString("base64")}`;

const response = await this.api.post("user-authenticators/passkey/authentication-options", {
json: {},
headers: {
Authorization: authorizationHeader,
},
});

return response.json();
}

async addAuthenticator({token, ...rest}: AddAuthenticatorRequest): Promise<AddAuthenticatorResponse> {
const response = await this.api.post("user-authenticators/passkey", {
json: rest,
headers: {
Authorization: `Bearer ${token}`,
},
});

return response.json();
}

async verify({token, ...rest}: VerifyRequest): Promise<VerifyResponse> {
const authorizationHeader = token ? `Bearer ${token}` : `Basic ${Buffer.from(this.tenantId).toString("base64")}`;

const response = await this.api.post("verify/passkey", {
json: rest,
headers: {
Authorization: authorizationHeader,
},
});

return response.json();
}
}
47 changes: 47 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/typescript-types";

export type RegistrationOptsRequest = {
userName?: string;
token: string;
};

export type RegistrationOptsResponse = {
challengeId: string;
options: PublicKeyCredentialCreationOptionsJSON;
};

export type AuthenticationOptsRequest = {
token?: string;
};

export type AuthenticationOptsResponse = {
challengeId: string;
options: PublicKeyCredentialCreationOptionsJSON;
};

export type AddAuthenticatorRequest = {
token: string;
challengeId: string;
registrationCredential: RegistrationResponseJSON;
};

export type AddAuthenticatorResponse = {
isVerified: boolean;
accessToken?: string;
userAuthenticatorId?: string;
};

export type VerifyRequest = {
token?: string;
challengeId: string;
authenticationCredential: AuthenticationResponseJSON;
};

export type VerifyResponse = {
isVerified: boolean;
accessToken?: string;
};
17 changes: 13 additions & 4 deletions src/authsignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,30 @@ import {
TokenPayload,
} from "./types";
import {PopupHandler} from "./popup-handler";
import {Passkey} from "./passkey";

const DEFAULT_COOKIE_NAME = "__as_aid";

const DEFAULT_BASE_URL = "https://challenge.authsignal.com/v1";

export class Authsignal {
anonymousId = "";
cookieDomain = "";
anonymousIdCookieName = "";
publishableKey = "";
passkey: Passkey;

private _token: string | undefined = undefined;

constructor({publishableKey, cookieDomain, cookieName}: AuthsignalOptions) {
this.publishableKey = publishableKey;
constructor({
cookieDomain,
cookieName = DEFAULT_COOKIE_NAME,
baseUrl = DEFAULT_BASE_URL,
tenantId,
}: AuthsignalOptions) {
this.cookieDomain = cookieDomain || getCookieDomain();
this.anonymousIdCookieName = cookieName || DEFAULT_COOKIE_NAME;
this.anonymousIdCookieName = cookieName;

this.passkey = new Passkey({tenantId, baseUrl});

const idCookie = getCookie(this.anonymousIdCookieName);

Expand Down
63 changes: 63 additions & 0 deletions src/passkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {startAuthentication, startRegistration} from "@simplewebauthn/browser";

import {PasskeyApiClient} from "./api";

type PasskeyOptions = {
baseUrl: string;
tenantId: string;
};

type SignUpParams = {
userName?: string;
token: string;
};

export class Passkey {
private api: PasskeyApiClient;

constructor({baseUrl, tenantId}: PasskeyOptions) {
this.api = new PasskeyApiClient({baseUrl, tenantId});
}

async signUp({userName, token}: SignUpParams) {
const optionsResponse = await this.api.registrationOptions({userName, token});

try {
const registrationResponse = await startRegistration(optionsResponse.options);

const addAuthenticatorResponse = await this.api.addAuthenticator({
challengeId: optionsResponse.challengeId,
registrationCredential: registrationResponse,
token,
});

return addAuthenticatorResponse?.accessToken;
} catch (error) {
console.error(error);
}
}

async signIn(params?: {token: string}): Promise<string | undefined>;
async signIn(params?: {autofill: boolean}): Promise<string | undefined>;
async signIn(params?: {token?: string; autofill?: boolean} | undefined) {
if (params?.token && params.autofill) {
throw new Error("Autofill is not supported when providing a token");
}

const optionsResponse = await this.api.authenticationOptions({token: params?.token});

try {
const authenticationResponse = await startAuthentication(optionsResponse.options, params?.autofill);

const verifyResponse = await this.api.verify({
challengeId: optionsResponse.challengeId,
authenticationCredential: authenticationResponse,
token: params?.token,
});

return verifyResponse?.accessToken;
} catch (error) {
console.error(error);
}
}
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ type PopupLaunchOptions = BaseLaunchOptions & {
export type LaunchOptions = RedirectLaunchOptions | PopupLaunchOptions;

export type AuthsignalOptions = {
publishableKey: string;
/**
* Cookie domain that will be used to identify
* users. If not set, location.hostname will be used
Expand All @@ -45,6 +44,8 @@ export type AuthsignalOptions = {
* Name of id cookie. `__as_aid` by default
*/
cookieName?: string;
baseUrl?: string;
tenantId: string;
};

export enum AuthsignalWindowMessage {
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"

"@simplewebauthn/browser@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-7.2.0.tgz#cb959290469d14b2f33e15156439e20caf539fc7"
integrity sha512-HHIvRPpqKy0UV/BsGAmx4rQRZuZTUFYLLH65FwpSOslqHruiHx3Ql/bq7A75bjWuJ296a+4BIAq3+SPaII01TQ==
dependencies:
"@simplewebauthn/typescript-types" "*"

"@simplewebauthn/typescript-types@*", "@simplewebauthn/typescript-types@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/@simplewebauthn/typescript-types/-/typescript-types-7.0.0.tgz#887178424220524e71c10ae4548fbb353ed58ae6"
integrity sha512-bV+xACCFTsrLR/23ozHO06ZllHZaxC8LlI5YCo79GvU2BrN+rePDU2yXwZIYndNWcMQwRdndRdAhpafOh9AC/g==

"@types/[email protected]":
version "0.0.39"
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
Expand Down Expand Up @@ -746,6 +758,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==

ky@^0.33.3:
version "0.33.3"
resolved "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543"
integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==

levn@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
Expand Down

0 comments on commit a48a5e6

Please sign in to comment.