Skip to content
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

Passkeys #28

Merged
merged 7 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,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, userName}: 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: {userName},
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();
}
}
48 changes: 48 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 = {
userName?: string;
token?: string;
};

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

export type AddAuthenticatorRequest = {
token: string;
challengeId: string;
authenticationCredential: 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
52 changes: 52 additions & 0 deletions src/passkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {startAuthentication, startRegistration} from "@simplewebauthn/browser";

import {PasskeyApiClient} from "./api";

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

export class Passkey {
private api: PasskeyApiClient;

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

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

try {
const attReponse = await startRegistration(optsResponse.options);

const addAuthenticatorResponse = await this.api.addAuthenticator({
challengeId: optsResponse.challengeId,
authenticationCredential: attReponse,
token,
});

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

async signIn({userName, token}: {userName?: string; token?: string}) {
const optsResponse = await this.api.authenticationOptions({userName, token});

try {
const asseReponse = await startAuthentication(optsResponse.options);

const verifyResponse = await this.api.verify({
challengeId: optsResponse.challengeId,
authenticationCredential: asseReponse,
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