Skip to content

Commit

Permalink
Guard against concurrent passkey autofill requests (#85)
Browse files Browse the repository at this point in the history
* guard against concurrent passkey autofill requests

* bump version

* Add console log for rp error

* export webauthnerror
  • Loading branch information
hwhmeikle authored Nov 11, 2024
1 parent 1144588 commit c44865c
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 102 deletions.
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.3.0",
"version": "1.4.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
11 changes: 11 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {WebAuthnError} from "@simplewebauthn/browser";
import {ErrorResponse} from "./api/types/shared";

type CookieOptions = {
Expand Down Expand Up @@ -79,3 +80,13 @@ export function handleApiResponse<T>(response: ErrorResponse | T) {
};
}
}

export function handleWebAuthnError(error: unknown) {
if (error instanceof WebAuthnError && error.code === "ERROR_INVALID_RP_ID") {
const rpId = error.message?.match(/"([^"]*)"/)?.[1] || "";

console.error(
`[Authsignal] The Relying Party ID "${rpId}" is invalid for this domain.\n To learn more, visit https://docs.authsignal.com/scenarios/passkeys-prebuilt-ui#defining-the-relying-party`
);
}
}
154 changes: 94 additions & 60 deletions src/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {startAuthentication, startRegistration} from "@simplewebauthn/browser";
import {PasskeyApiClient} from "./api";
import {AuthenticationResponseJSON, RegistrationResponseJSON, AuthenticatorAttachment} from "@simplewebauthn/types";
import {TokenCache} from "./token-cache";
import {handleErrorResponse} from "./helpers";
import {handleErrorResponse, handleWebAuthnError} from "./helpers";
import {AuthsignalResponse} from "./types";

type PasskeyOptions = {
Expand Down Expand Up @@ -43,6 +43,8 @@ type SignInResponse = {
authenticationResponse?: AuthenticationResponseJSON;
};

let autofillRequestPending = false;

export class Passkey {
public api: PasskeyApiClient;
private passkeyLocalStorageKey = "as_user_passkey_map";
Expand Down Expand Up @@ -80,35 +82,43 @@ export class Passkey {
return handleErrorResponse(optionsResponse);
}

const registrationResponse = await startRegistration({optionsJSON: optionsResponse.options, useAutoRegister});
try {
const registrationResponse = await startRegistration({optionsJSON: optionsResponse.options, useAutoRegister});

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

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

if (addAuthenticatorResponse.isVerified) {
this.storeCredentialAgainstDevice({
...registrationResponse,
userId: addAuthenticatorResponse.userId,
});
}
if (addAuthenticatorResponse.isVerified) {
this.storeCredentialAgainstDevice({
...registrationResponse,
userId: addAuthenticatorResponse.userId,
});
}

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

return {
data: {
token: addAuthenticatorResponse.accessToken,
registrationResponse,
},
};
return {
data: {
token: addAuthenticatorResponse.accessToken,
registrationResponse,
},
};
} catch (e) {
autofillRequestPending = false;

handleWebAuthnError(e);

throw e;
}
}

async signIn(params?: SignInParams): Promise<AuthsignalResponse<SignInResponse>> {
Expand All @@ -120,9 +130,19 @@ export class Passkey {
throw new Error("action is not supported when providing a token");
}

if (params?.autofill) {
if (autofillRequestPending) {
return {};
} else {
autofillRequestPending = true;
}
}

const challengeResponse = params?.action ? await this.api.challenge(params.action) : null;

if (challengeResponse && "error" in challengeResponse) {
autofillRequestPending = false;

return handleErrorResponse(challengeResponse);
}

Expand All @@ -132,50 +152,64 @@ export class Passkey {
});

if ("error" in optionsResponse) {
autofillRequestPending = false;

return handleErrorResponse(optionsResponse);
}

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

if (params?.onVerificationStarted) {
params.onVerificationStarted();
}
if (params?.onVerificationStarted) {
params.onVerificationStarted();
}

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

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

if (verifyResponse.isVerified) {
this.storeCredentialAgainstDevice({...authenticationResponse, userId: verifyResponse.userId});
}
return handleErrorResponse(verifyResponse);
}

if (verifyResponse.accessToken) {
this.cache.token = verifyResponse.accessToken;
}
if (verifyResponse.isVerified) {
this.storeCredentialAgainstDevice({...authenticationResponse, userId: verifyResponse.userId});
}

const {accessToken: token, userId, userAuthenticatorId, username, userDisplayName, isVerified} = verifyResponse;

return {
data: {
isVerified,
token,
userId,
userAuthenticatorId,
username,
displayName: userDisplayName,
authenticationResponse,
},
};
if (verifyResponse.accessToken) {
this.cache.token = verifyResponse.accessToken;
}

const {accessToken: token, userId, userAuthenticatorId, username, userDisplayName, isVerified} = verifyResponse;

autofillRequestPending = false;

return {
data: {
isVerified,
token,
userId,
userAuthenticatorId,
username,
displayName: userDisplayName,
authenticationResponse,
},
};
} catch (e) {
autofillRequestPending = false;

handleWebAuthnError(e);

throw e;
}
}

async isAvailableOnDevice({userId}: {userId: string}) {
Expand Down
96 changes: 55 additions & 41 deletions src/security-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {startAuthentication, startRegistration} from "@simplewebauthn/browser";

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

Expand Down Expand Up @@ -46,27 +46,33 @@ export class SecurityKey {
return handleErrorResponse(optionsResponse);
}

const registrationResponse = await startRegistration({optionsJSON: optionsResponse});
try {
const registrationResponse = await startRegistration({optionsJSON: optionsResponse});

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

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

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

return {
data: {
token: addAuthenticatorResponse.accessToken,
registrationResponse,
},
};
return {
data: {
token: addAuthenticatorResponse.accessToken,
registrationResponse,
},
};
} catch (e) {
handleWebAuthnError(e);

throw e;
}
}

async verify(): Promise<AuthsignalResponse<VerifyResponse>> {
Expand All @@ -82,29 +88,37 @@ export class SecurityKey {
return handleErrorResponse(optionsResponse);
}

const authenticationResponse = await startAuthentication({optionsJSON: 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;
try {
const authenticationResponse = await startAuthentication({
optionsJSON: 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,
},
};
} catch (e) {
handleWebAuthnError(e);

throw e;
}

const {accessToken: token, isVerified} = verifyResponse;

return {
data: {
isVerified,
token,
authenticationResponse,
},
};
}
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {WebAuthnError} from "@simplewebauthn/browser";

type BaseLaunchOptions = {
/**
* How the Authsignal Prebuilt MFA page should launch.
Expand Down Expand Up @@ -103,3 +105,5 @@ export type CheckVerificationStatusResponse = {
isVerified: boolean;
token?: string;
};

export {WebAuthnError};

0 comments on commit c44865c

Please sign in to comment.