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

Allow passing custom AbortController to register and authenticate functions without breaking existing behavior #79

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
47 changes: 35 additions & 12 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthenticateOptions, AuthenticationJSON, Base64URLString, CredentialDescriptor, ExtendedAuthenticatorTransport, PublicKeyCredentialHints, RegisterOptions, RegistrationJSON, User, WebAuthnCreateOptions, WebAuthnGetOptions } from './types.js'
import { AuthenticateOptions, AuthenticationJSON, Base64URLString, CredentialDescriptor, PublicKeyCredentialHints, RegisterOptions, RegistrationJSON, User, WebAuthnCreateOptions, WebAuthnGetOptions } from './types.js'
import * as utils from './utils'

/**
Expand Down Expand Up @@ -53,6 +53,9 @@ let ongoingAuth: AbortController | null = null;
* @param {'discouraged'|'preferred'|'required'} [discoverable] A "discoverable" credential can be selected using `authenticate(...)` without providing credential IDs.
* Instead, a native pop-up will appear for user selection.
* This may have an impact on the "passkeys" user experience and syncing behavior of the key.
* @param {AbortSignal} [signal] An optional AbortSignal to allow aborting the registration process.
* If not provided, an `AbortController` (stored in `ongoingAuth`) is used to cancel any previous ongoing authentication or registration
* if another `authenticate()` or `register()` call is made.
*/
export async function register(options: RegisterOptions): Promise<RegistrationJSON> {

Expand Down Expand Up @@ -97,14 +100,23 @@ export async function register(options: RegisterOptions): Promise<RegistrationJS

console.debug(creationOptions)

if (ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication')
ongoingAuth = new AbortController();
let signal: AbortSignal;
if (options.signal) {
// Use the provided signal
signal = options.signal;
} else {
// Use the global ongoingAuth
if (ongoingAuth != null) {
ongoingAuth.abort('Cancel ongoing authentication');
}
ongoingAuth = new AbortController();
signal = ongoingAuth.signal;
}

const raw = await navigator.credentials.create({
publicKey: creationOptions,
signal: ongoingAuth?.signal
}) as PublicKeyCredential
signal: signal,
}) as PublicKeyCredential;
const response = raw.response as AuthenticatorAttestationResponse

ongoingAuth = null;
Expand Down Expand Up @@ -150,6 +162,9 @@ export async function isAutocompleteAvailable() {
* @param {number} [timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.
* @param {'required'|'preferred'|'discouraged'} [userVerification='required'] Whether to prompt for biometric/PIN check or not.
* @param {boolean} [conditional] Does not return directly, but only when the user has selected a credential in the input field with `autocomplete="username webauthn"`
* @param {AbortSignal} [signal] An optional signal to abort the authentication request.
* If not provided, an `AbortController` (stored in `ongoingAuth`) is used to cancel any previous ongoing authentication or registration
* if another `authenticate()` or `register()` call is made.
*/
export async function authenticate(options: AuthenticateOptions): Promise<AuthenticationJSON> {
if (!utils.isBase64url(options.challenge))
Expand All @@ -169,16 +184,24 @@ export async function authenticate(options: AuthenticateOptions): Promise<Authen

console.debug(authOptions)

if(ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication')

ongoingAuth = new AbortController();
let signal: AbortSignal;
if (options.signal) {
// Use the provided signal
signal = options.signal;
} else {
// Use the global ongoingAuth
if (ongoingAuth != null) {
ongoingAuth.abort('Cancel ongoing authentication');
}
ongoingAuth = new AbortController();
signal = ongoingAuth.signal;
}

const raw = await navigator.credentials.get({
publicKey: authOptions,
mediation: options.autocomplete ? 'conditional' : undefined,
signal: ongoingAuth?.signal
}) as PublicKeyCredential
signal: signal,
}) as PublicKeyCredential;

if (raw.type != "public-key")
throw "Unexpected credential type!";
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface CommonOptions {
export interface RegisterOptions extends CommonOptions {
attestation?: boolean
discoverable?: ResidentKeyRequirement
signal?: AbortSignal
user: string | User
}

Expand All @@ -56,6 +57,7 @@ export interface CredentialDescriptor {
export interface AuthenticateOptions extends CommonOptions {
allowCredentials?: (CredentialDescriptor | string)[]
autocomplete?: boolean
signal?: AbortSignal
}


Expand Down