Skip to content

Commit afec179

Browse files
authoredMar 25, 2024··
chore(clerk-js): Improve error handling of passkeys (#3025)
* chore(clerk-js): Improve error handling in retrieval of passkeys * chore(clerk-js): Add changeset * chore(clerk-js): Improve error handling in creation of passkeys * chore(clerk-js): Update usage * chore(clerk-js): Fix typos * chore(clerk-js): Add changeset * chore(clerk-js): Add changeset
1 parent 71319f1 commit afec179

File tree

8 files changed

+106
-35
lines changed

8 files changed

+106
-35
lines changed
 

‎.changeset/eight-buttons-wink.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@clerk/localizations': minor
3+
'@clerk/clerk-js': minor
4+
'@clerk/types': minor
5+
---
6+
7+
Improved error handling for registration and retrieval of passkeys.
8+
ClerkRuntimeError codes introduced:
9+
- `passkey_not_supported`
10+
- `passkeys_pa_not_supported`
11+
- `passkey_invalid_rpID_or_domain`
12+
- `passkey_already_exists`
13+
- `passkey_operation_aborted`
14+
- `passkey_retrieval_cancelled`
15+
- `passkey_retrieval_failed`
16+
- `passkey_registration_cancelled`
17+
- `passkey_registration_failed`
18+
19+
Example usage:
20+
21+
```ts
22+
try {
23+
await __experimental_authenticateWithPasskey(...args);
24+
}catch (e) {
25+
if (isClerkRuntimeError(e)) {
26+
if (err.code === 'passkey_operation_aborted') {
27+
...
28+
}
29+
}
30+
}
31+
32+
33+
```

‎packages/clerk-js/src/core/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,9 @@ export function clerkInvalidRoutingStrategy(strategy?: string): never {
113113
export function clerkUnsupportedReloadMethod(className: string): never {
114114
throw new Error(`${errorPrefix} Calling ${className}.reload is not currently supported. Please contact support.`);
115115
}
116+
117+
export function clerkMissingWebAuthnPublicKeyOptions(name: 'create' | 'get'): never {
118+
throw new Error(
119+
`${errorPrefix} Missing publicKey. When calling 'navigator.credentials.${name}()' it is required to pass a publicKey object.`,
120+
);
121+
}

‎packages/clerk-js/src/core/resources/Passkey.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import type {
1010

1111
import { unixEpochToDate } from '../../utils/date';
1212
import {
13+
ClerkWebAuthnError,
1314
isWebAuthnPlatformAuthenticatorSupported,
1415
isWebAuthnSupported,
1516
serializePublicKeyCredential,
1617
webAuthnCreateCredential,
1718
} from '../../utils/passkeys';
18-
import { BaseResource, ClerkRuntimeError, DeletedObject, PasskeyVerification } from './internal';
19+
import { clerkMissingWebAuthnPublicKeyOptions } from '../errors';
20+
import { BaseResource, DeletedObject, PasskeyVerification } from './internal';
1921

2022
export class Passkey extends BaseResource implements PasskeyResource {
2123
id!: string;
@@ -60,8 +62,8 @@ export class Passkey extends BaseResource implements PasskeyResource {
6062
* As a precaution we need to check if WebAuthn is supported.
6163
*/
6264
if (!isWebAuthnSupported()) {
63-
throw new ClerkRuntimeError('Passkeys are not supported', {
64-
code: 'passkeys_unsupported',
65+
throw new ClerkWebAuthnError('Passkeys are not supported on this device.', {
66+
code: 'passkey_not_supported',
6567
});
6668
}
6769

@@ -73,15 +75,17 @@ export class Passkey extends BaseResource implements PasskeyResource {
7375

7476
// This should never occur, just a fail-safe
7577
if (!publicKey) {
76-
// TODO-PASSKEYS: Implement this later
77-
throw 'Missing key';
78+
clerkMissingWebAuthnPublicKeyOptions('create');
7879
}
7980

8081
if (publicKey.authenticatorSelection?.authenticatorAttachment === 'platform') {
8182
if (!(await isWebAuthnPlatformAuthenticatorSupported())) {
82-
throw new ClerkRuntimeError('Platform authenticator is not supported', {
83-
code: 'passkeys_unsupported_platform_authenticator',
84-
});
83+
throw new ClerkWebAuthnError(
84+
'Registration requires a platform authenticator but the device does not support it.',
85+
{
86+
code: 'passkeys_pa_not_supported',
87+
},
88+
);
8589
}
8690
}
8791

‎packages/clerk-js/src/core/resources/SignIn.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ClerkRuntimeError, deepSnakeToCamel, Poller } from '@clerk/shared';
1+
import { deepSnakeToCamel, Poller } from '@clerk/shared';
22
import type {
33
__experimental_PasskeyFactor,
44
AttemptFirstFactorParams,
@@ -30,6 +30,7 @@ import type {
3030

3131
import { generateSignatureWithMetamask, getMetamaskIdentifier, windowNavigate } from '../../utils';
3232
import {
33+
ClerkWebAuthnError,
3334
convertJSONToPublicKeyRequestOptions,
3435
isWebAuthnAutofillSupported,
3536
isWebAuthnSupported,
@@ -41,6 +42,7 @@ import {
4142
clerkInvalidFAPIResponse,
4243
clerkInvalidStrategy,
4344
clerkMissingOptionError,
45+
clerkMissingWebAuthnPublicKeyOptions,
4446
clerkVerifyEmailAddressCalledBeforeCreate,
4547
clerkVerifyPasskeyCalledBeforeCreate,
4648
clerkVerifyWeb3WalletCalledBeforeCreate,
@@ -271,8 +273,8 @@ export class SignIn extends BaseResource implements SignInResource {
271273
* As a precaution we need to check if WebAuthn is supported.
272274
*/
273275
if (!isWebAuthnSupported()) {
274-
throw new ClerkRuntimeError('Passkeys are not supported', {
275-
code: 'passkeys_unsupported',
276+
throw new ClerkWebAuthnError('Passkeys are not supported', {
277+
code: 'passkey_not_supported',
276278
});
277279
}
278280

@@ -294,11 +296,10 @@ export class SignIn extends BaseResource implements SignInResource {
294296
}
295297

296298
const { nonce } = this.firstFactorVerification;
297-
const publicKey = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null;
299+
const publicKeyOptions = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null;
298300

299-
if (!publicKey) {
300-
// TODO-PASSKEYS: Implement this later
301-
throw 'Missing key';
301+
if (!publicKeyOptions) {
302+
clerkMissingWebAuthnPublicKeyOptions('get');
302303
}
303304

304305
let canUseConditionalUI = false;
@@ -311,9 +312,9 @@ export class SignIn extends BaseResource implements SignInResource {
311312
canUseConditionalUI = await isWebAuthnAutofillSupported();
312313
}
313314

314-
// Invoke the WebAuthn get() method.
315+
// Invoke the navigator.create.get() method.
315316
const { publicKeyCredential, error } = await webAuthnGetCredential({
316-
publicKeyOptions: publicKey,
317+
publicKeyOptions,
317318
conditionalUI: canUseConditionalUI,
318319
});
319320

‎packages/clerk-js/src/ui/components/SignIn/shared.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ function useHandleAuthenticateWithPasskey(onSecondFactor: () => Promise<unknown>
3535
}
3636
} catch (err) {
3737
const { flow } = args[0] || {};
38-
// In case of autofill, if retrieval of credentials is aborted just return to avoid updating state of unmounted components.
39-
if (flow === 'autofill' && isClerkRuntimeError(err)) {
40-
const skipActionCodes = ['passkey_retrieval_aborted', 'passkey_retrieval_cancelled'];
41-
if (skipActionCodes.includes(err.code)) {
38+
39+
if (isClerkRuntimeError(err)) {
40+
// In any case if the call gets aborted we should skip showing an error. This prevents updating the state of unmounted components.
41+
if (err.code === 'passkey_operation_aborted') {
42+
return;
43+
}
44+
// In case of autofill, if retrieval of credentials is cancelled by the user avoid showing errors as it results to pour UX.
45+
if (flow === 'autofill' && err.code === 'passkey_retrieval_cancelled') {
4246
return;
4347
}
4448
}

‎packages/clerk-js/src/utils/passkeys.ts

+27-14
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ type WebAuthnGetCredentialReturn =
2525
CredentialReturn<__experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse>;
2626

2727
type ClerkWebAuthnErrorCode =
28-
| 'passkey_exists'
29-
| 'passkey_retrieval_aborted'
28+
// Generic
29+
| 'passkey_not_supported'
30+
| 'passkeys_pa_not_supported'
31+
| 'passkey_invalid_rpID_or_domain'
32+
| 'passkey_already_exists'
33+
| 'passkey_operation_aborted'
34+
// Retrieval
3035
| 'passkey_retrieval_cancelled'
36+
| 'passkey_retrieval_failed'
37+
// Registration
3138
| 'passkey_registration_cancelled'
32-
| 'passkey_credential_create_failed'
33-
| 'passkey_credential_get_failed';
39+
| 'passkey_registration_failed';
3440

3541
function isWebAuthnSupported() {
3642
return (
@@ -92,7 +98,7 @@ async function webAuthnCreateCredential(
9298
if (!credential) {
9399
return {
94100
error: new ClerkWebAuthnError('Browser failed to create credential', {
95-
code: 'passkey_credential_create_failed',
101+
code: 'passkey_registration_failed',
96102
}),
97103
publicKeyCredential: null,
98104
};
@@ -148,7 +154,7 @@ async function webAuthnGetCredential({
148154

149155
if (!credential) {
150156
return {
151-
error: new ClerkWebAuthnError('Browser failed to get credential', { code: 'passkey_credential_get_failed' }),
157+
error: new ClerkWebAuthnError('Browser failed to get credential', { code: 'passkey_retrieval_failed' }),
152158
publicKeyCredential: null,
153159
};
154160
}
@@ -159,33 +165,40 @@ async function webAuthnGetCredential({
159165
}
160166
}
161167

168+
function handlePublicKeyError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
169+
if (error.name === 'AbortError') {
170+
return new ClerkWebAuthnError(error.message, { code: 'passkey_operation_aborted' });
171+
}
172+
if (error.name === 'SecurityError') {
173+
return new ClerkWebAuthnError(error.message, { code: 'passkey_invalid_rpID_or_domain' });
174+
}
175+
return error;
176+
}
177+
162178
/**
163179
* Map webauthn errors from `navigator.credentials.create()` to Clerk-js errors
164180
* @param error
165181
*/
166182
function handlePublicKeyCreateError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
167183
if (error.name === 'InvalidStateError') {
168184
// Note: Firefox will throw 'NotAllowedError' when passkeys exists
169-
return new ClerkWebAuthnError(error.message, { code: 'passkey_exists' });
170-
} else if (error.name === 'NotAllowedError') {
185+
return new ClerkWebAuthnError(error.message, { code: 'passkey_already_exists' });
186+
}
187+
if (error.name === 'NotAllowedError') {
171188
return new ClerkWebAuthnError(error.message, { code: 'passkey_registration_cancelled' });
172189
}
173-
return error;
190+
return handlePublicKeyError(error);
174191
}
175192

176193
/**
177194
* Map webauthn errors from `navigator.credentials.get()` to Clerk-js errors
178195
* @param error
179196
*/
180197
function handlePublicKeyGetError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
181-
if (error.name === 'AbortError') {
182-
return new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_aborted' });
183-
}
184-
185198
if (error.name === 'NotAllowedError') {
186199
return new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_cancelled' });
187200
}
188-
return error;
201+
return handlePublicKeyError(error);
189202
}
190203

191204
function convertJSONToPublicKeyCreateOptions(jsonPublicKey: PublicKeyCredentialCreationOptionsJSON) {

‎packages/localizations/src/en-US.ts

+5
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,11 @@ export const enUS: LocalizationResource = {
442442
'Sign up unsuccessful due to failed security validations. Please refresh the page to try again or reach out to support for more assistance.',
443443
captcha_unavailable:
444444
'Sign up unsuccessful due to failed bot validation. Please refresh the page to try again or reach out to support for more assistance.',
445+
passkey_not_supported: 'Passkeys are not supported on this device.',
446+
passkeys_pa_not_supported: 'Registration requires a platform authenticator but the device does not support it.',
447+
passkey_retrieval_cancelled: 'Passkey verification was cancelled or timed out.',
448+
passkey_registration_cancelled: 'Passkey registration was cancelled or timed out.',
449+
passkey_already_exists: 'A passkey is already registered with this device.',
445450
form_code_incorrect: '',
446451
form_identifier_exists: '',
447452
form_identifier_not_found: '',

‎packages/types/src/localization.ts

+5
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,11 @@ type UnstableErrors = WithParamName<{
723723
form_identifier_not_found: LocalizationValue;
724724
captcha_unavailable: LocalizationValue;
725725
captcha_invalid: LocalizationValue;
726+
passkey_not_supported: LocalizationValue;
727+
passkeys_pa_not_supported: LocalizationValue;
728+
passkey_retrieval_cancelled: LocalizationValue;
729+
passkey_registration_cancelled: LocalizationValue;
730+
passkey_already_exists: LocalizationValue;
726731
form_password_pwned: LocalizationValue;
727732
form_username_invalid_length: LocalizationValue;
728733
form_username_invalid_character: LocalizationValue;

0 commit comments

Comments
 (0)
Please sign in to comment.