Skip to content

Commit

Permalink
Add security key support (#82)
Browse files Browse the repository at this point in the history
* wip

* wip

* Fix endpoint
  • Loading branch information
hwhmeikle authored Oct 24, 2024
1 parent eb2d16d commit e6e36fb
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@authsignal/browser",
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
93 changes: 93 additions & 0 deletions src/api/security-key-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/types";
import {buildHeaders, handleTokenExpired} from "./helpers";
import {AddAuthenticatorResponse, ErrorResponse, VerifyResponse} from "./types/passkey";
import {ApiClientOptions} from "./types/shared";

export class SecurityKeyApiClient {
tenantId: string;
baseUrl: string;
onTokenExpired?: () => void;

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

async registrationOptions({token}: {token: string}): Promise<PublicKeyCredentialCreationOptionsJSON | ErrorResponse> {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/security-key/registration-options`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify({}),
});

const responseJson = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async authenticationOptions({
token,
}: {
token?: string;
}): Promise<PublicKeyCredentialRequestOptionsJSON | ErrorResponse> {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/security-key/authentication-options`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify({}),
});

const responseJson = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async addAuthenticator({
token,
registrationCredential,
}: {
token: string;
registrationCredential: RegistrationResponseJSON;
}): Promise<AddAuthenticatorResponse | ErrorResponse> {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/security-key`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(registrationCredential),
});

const responseJson = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async verify({
token,
authenticationCredential,
}: {
token?: string;
authenticationCredential: AuthenticationResponseJSON;
}): Promise<VerifyResponse | ErrorResponse> {
const response = await fetch(`${this.baseUrl}/client/verify/security-key`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(authenticationCredential),
});

const responseJson = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}
}
3 changes: 3 additions & 0 deletions src/authsignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {TokenCache} from "./token-cache";
import {Email} from "./email";
import {Sms} from "./sms";
import {EmailMagicLink} from "./email-magic-link";
import {SecurityKey} from "./security-key";

const DEFAULT_COOKIE_NAME = "__as_aid";
const DEFAULT_PROFILING_COOKIE_NAME = "__as_pid";
Expand All @@ -36,6 +37,7 @@ export class Authsignal {
email: Email;
emailML: EmailMagicLink;
sms: Sms;
securityKey: SecurityKey;

constructor({
cookieDomain,
Expand Down Expand Up @@ -72,6 +74,7 @@ export class Authsignal {
this.email = new Email({tenantId, baseUrl, onTokenExpired});
this.emailML = new EmailMagicLink({tenantId, baseUrl, onTokenExpired});
this.sms = new Sms({tenantId, baseUrl, onTokenExpired});
this.securityKey = new SecurityKey({tenantId, baseUrl, onTokenExpired});
}

setToken(token: string) {
Expand Down
110 changes: 110 additions & 0 deletions src/security-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {startAuthentication, startRegistration} from "@simplewebauthn/browser";

import {AuthenticationResponseJSON, RegistrationResponseJSON} from "@simplewebauthn/types";
import {TokenCache} from "./token-cache";
import {handleErrorResponse} from "./helpers";
import {AuthsignalResponse} from "./types";
import {SecurityKeyApiClient} from "./api/security-key-api-client";

type SecurityKeyOptions = {
baseUrl: string;
tenantId: string;
onTokenExpired?: () => void;
};

type EnrollResponse = {
token?: string;
registrationResponse?: RegistrationResponseJSON;
};

type VerifyResponse = {
isVerified: boolean;
token?: string;
authenticationResponse?: AuthenticationResponseJSON;
};

export class SecurityKey {
public api: SecurityKeyApiClient;
private cache = TokenCache.shared;

constructor({baseUrl, tenantId, onTokenExpired}: SecurityKeyOptions) {
this.api = new SecurityKeyApiClient({baseUrl, tenantId, onTokenExpired});
}

async enroll(): Promise<AuthsignalResponse<EnrollResponse>> {
if (!this.cache.token) {
return this.cache.handleTokenNotSetError();
}

const optionsInput = {
token: this.cache.token,
};

const optionsResponse = await this.api.registrationOptions(optionsInput);

if ("error" in optionsResponse) {
return handleErrorResponse(optionsResponse);
}

const registrationResponse = await startRegistration(optionsResponse);

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

if ("error" in addAuthenticatorResponse) {
return handleErrorResponse(addAuthenticatorResponse);
}

if (addAuthenticatorResponse.accessToken) {
this.cache.token = addAuthenticatorResponse.accessToken;
}

return {
data: {
token: addAuthenticatorResponse.accessToken,
registrationResponse,
},
};
}

async verify(): Promise<AuthsignalResponse<VerifyResponse>> {
if (!this.cache.token) {
return this.cache.handleTokenNotSetError();
}

const optionsResponse = await this.api.authenticationOptions({
token: this.cache.token,
});

if ("error" in optionsResponse) {
return handleErrorResponse(optionsResponse);
}

const authenticationResponse = await startAuthentication(optionsResponse);

const verifyResponse = await this.api.verify({
authenticationCredential: authenticationResponse,
token: this.cache.token,
});

if ("error" in verifyResponse) {
return handleErrorResponse(verifyResponse);
}

if (verifyResponse.accessToken) {
this.cache.token = verifyResponse.accessToken;
}

const {accessToken: token, isVerified} = verifyResponse;

return {
data: {
isVerified,
token,
authenticationResponse,
},
};
}
}

0 comments on commit e6e36fb

Please sign in to comment.