FIX: Broken login with security key when passkeys enabled (#24249)

In some browsers, 2FA login was failing with a "request is already
pending" error. This only applied when `experimental_passkeys` was
enabled and on Chrome only. This was due to the fact that the webauthn
API only supports one auth attempt at a time, so the security key call
needs to abort the passkey's conditional UI request before starting.

I am not sure we can test this. We have system specs that simulate
webauthn credentials and they didn't catch this (probably because the
simulation covers the whole flow).
This commit is contained in:
Penar Musaraj
2023-11-06 16:45:33 -05:00
committed by GitHub
parent 1dd8bd2ad9
commit fcaedbf4ba

View File

@@ -17,6 +17,27 @@ export function isWebauthnSupported() {
return typeof PublicKeyCredential !== "undefined";
}
// The webauthn API only supports one auth attempt at a time
// We need this service to cancel the previous attempt when a new one is started
class WebauthnAbortService {
controller = undefined;
signal() {
if (this.controller) {
const abortError = new Error("Cancelling pending webauthn call");
abortError.name = "AbortError";
this.controller.abort(abortError);
}
this.controller = new AbortController();
return this.controller.signal;
}
}
// Need to use a singleton here to reset the active webauthn ceremony
// Inspired by the BaseWebAuthnAbortService in https://github.com/MasterKale/SimpleWebAuthn
const WebauthnAbortHandler = new WebauthnAbortService();
export function getWebauthnCredential(
challenge,
allowedCredentialIds,
@@ -49,6 +70,7 @@ export function getWebauthnCredential(
// (this is only a hint, though, browser may still prompt)
userVerification: "discouraged",
},
signal: WebauthnAbortHandler.signal(),
})
.then((credential) => {
// 3. If credential.response is not an instance of AuthenticatorAssertionResponse, abort the ceremony.
@@ -94,27 +116,6 @@ export function getWebauthnCredential(
});
}
// The webauthn API only supports one auth attempt at a time
// We need this service to cancel the previous attempt when a new one is started
class WebauthnAbortService {
controller = undefined;
signal() {
if (this.controller) {
const abortError = new Error("Cancelling pending webauthn call");
abortError.name = "AbortError";
this.controller.abort(abortError);
}
this.controller = new AbortController();
return this.controller.signal;
}
}
// Need to use a singleton here to reset the active webauthn ceremony
// Inspired by the BaseWebAuthnAbortService in https://github.com/MasterKale/SimpleWebAuthn
const WebauthnAbortHandler = new WebauthnAbortService();
export async function getPasskeyCredential(
challenge,
errorCallback,