Skip to content

Commit 1f650f3

Browse files
authoredMar 14, 2024··
feat(clerk-js): Experimental method for authenticating with a passkey (#2970)
* feat(clerk-js): Experimental method for authenticating with a passkey * fix(clerk-js): Pass correct object to `attemptFirstFactor` * chore(clerk-js): Export types as experimental * chore(clerk-js): Export types as experimental * chore(clerk-js): Add changeset * fix(clerk-js): Do not spread object with buffers * test(clerk-js): Tests for new utilities * chore(clerk-js): Update error code
1 parent c8c333c commit 1f650f3

File tree

13 files changed

+349
-41
lines changed

13 files changed

+349
-41
lines changed
 

‎.changeset/light-ligers-beam.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
Experimental support for authenticating with a passkey.
7+
Example usage: `await signIn.authenticateWithPasskey()`.

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

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export function clerkVerifyWeb3WalletCalledBeforeCreate(type: 'SignIn' | 'SignUp
5858
);
5959
}
6060

61+
export function clerkVerifyPasskeyCalledBeforeCreate(): never {
62+
throw new Error(
63+
`${errorPrefix} You need to start a SignIn flow by calling SignIn.create({ strategy: 'passkey' }) first`,
64+
);
65+
}
66+
6167
export function clerkMissingOptionError(name = ''): never {
6268
throw new Error(`${errorPrefix} Missing '${name}' option`);
6369
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
__experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
23
DeletedObjectJSON,
34
DeletedObjectResource,
45
PasskeyJSON,
@@ -8,7 +9,6 @@ import type {
89
} from '@clerk/types';
910

1011
import { unixEpochToDate } from '../../utils/date';
11-
import type { PublicKeyCredentialWithAuthenticatorAttestationResponse } from '../../utils/passkeys';
1212
import {
1313
isWebAuthnPlatformAuthenticatorSupported,
1414
isWebAuthnSupported,
@@ -41,7 +41,7 @@ export class Passkey extends BaseResource implements PasskeyResource {
4141

4242
private static async attemptVerification(
4343
passkeyId: string,
44-
credential: PublicKeyCredentialWithAuthenticatorAttestationResponse,
44+
credential: __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
4545
) {
4646
const jsonPublicKeyCredential = serializePublicKeyCredential(credential);
4747
return BaseResource._fetch({

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

+84-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { deepSnakeToCamel, Poller } from '@clerk/shared';
1+
import { ClerkRuntimeError, deepSnakeToCamel, Poller } from '@clerk/shared';
22
import type {
3+
__experimental_PasskeyFactor,
34
AttemptFirstFactorParams,
45
AttemptSecondFactorParams,
56
AuthenticateWithRedirectParams,
@@ -28,12 +29,20 @@ import type {
2829
} from '@clerk/types';
2930

3031
import { generateSignatureWithMetamask, getMetamaskIdentifier, windowNavigate } from '../../utils';
32+
import {
33+
convertJSONToPublicKeyRequestOptions,
34+
isWebAuthnAutofillSupported,
35+
isWebAuthnSupported,
36+
serializePublicKeyCredentialAssertion,
37+
webAuthnGetCredential,
38+
} from '../../utils/passkeys';
3139
import { createValidatePassword } from '../../utils/passwords/password';
3240
import {
3341
clerkInvalidFAPIResponse,
3442
clerkInvalidStrategy,
3543
clerkMissingOptionError,
3644
clerkVerifyEmailAddressCalledBeforeCreate,
45+
clerkVerifyPasskeyCalledBeforeCreate,
3746
clerkVerifyWeb3WalletCalledBeforeCreate,
3847
} from '../errors';
3948
import { BaseResource, UserData, Verification } from './internal';
@@ -74,6 +83,11 @@ export class SignIn extends BaseResource implements SignInResource {
7483
prepareFirstFactor = (factor: PrepareFirstFactorParams): Promise<SignInResource> => {
7584
let config;
7685
switch (factor.strategy) {
86+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
87+
case 'passkey':
88+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
89+
config = {} as PassKeyConfig;
90+
break;
7791
case 'email_link':
7892
config = {
7993
emailAddressId: factor.emailAddressId,
@@ -113,9 +127,22 @@ export class SignIn extends BaseResource implements SignInResource {
113127
});
114128
};
115129

116-
attemptFirstFactor = (params: AttemptFirstFactorParams): Promise<SignInResource> => {
130+
attemptFirstFactor = (attemptFactor: AttemptFirstFactorParams): Promise<SignInResource> => {
131+
let config;
132+
switch (attemptFactor.strategy) {
133+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
134+
case 'passkey':
135+
config = {
136+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
137+
publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(attemptFactor.publicKeyCredential)),
138+
};
139+
break;
140+
default:
141+
config = { ...attemptFactor };
142+
}
143+
117144
return this._basePost({
118-
body: params,
145+
body: { ...config, strategy: attemptFactor.strategy },
119146
action: 'attempt_first_factor',
120147
});
121148
};
@@ -234,6 +261,60 @@ export class SignIn extends BaseResource implements SignInResource {
234261
});
235262
};
236263

264+
public __experimental_authenticateWithPasskey = async (): Promise<SignInResource> => {
265+
/**
266+
* The UI should always prevent from this method being called if WebAuthn is not supported.
267+
* As a precaution we need to check if WebAuthn is supported.
268+
*/
269+
if (!isWebAuthnSupported()) {
270+
throw new ClerkRuntimeError('Passkeys are not supported', {
271+
code: 'passkeys_unsupported',
272+
});
273+
}
274+
275+
if (!this.firstFactorVerification.nonce) {
276+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
277+
await this.create({ strategy: 'passkey' });
278+
}
279+
280+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
281+
const passKeyFactor = this.supportedFirstFactors.find(
282+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
283+
f => f.strategy === 'passkey',
284+
) as __experimental_PasskeyFactor;
285+
286+
if (!passKeyFactor) {
287+
clerkVerifyPasskeyCalledBeforeCreate();
288+
}
289+
290+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
291+
await this.prepareFirstFactor(passKeyFactor);
292+
293+
const { nonce } = this.firstFactorVerification;
294+
const publicKey = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null;
295+
296+
if (!publicKey) {
297+
// TODO-PASSKEYS: Implement this later
298+
throw 'Missing key';
299+
}
300+
301+
// Invoke the WebAuthn get() method.
302+
const { publicKeyCredential, error } = await webAuthnGetCredential({
303+
publicKeyOptions: publicKey,
304+
conditionalUI: await isWebAuthnAutofillSupported(),
305+
});
306+
307+
if (!publicKeyCredential) {
308+
throw error;
309+
}
310+
311+
return this.attemptFirstFactor({
312+
publicKeyCredential,
313+
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
314+
strategy: 'passkey',
315+
});
316+
};
317+
237318
validatePassword: ReturnType<typeof createValidatePassword> = (password, cb) => {
238319
if (SignIn.clerk.__unstable__environment?.userSettings.passwordSettings) {
239320
return createValidatePassword({

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { parseError } from '@clerk/shared/error';
22
import type {
3+
__experimental_PublicKeyCredentialCreationOptionsWithoutExtensions,
34
ClerkAPIError,
45
PasskeyVerificationResource,
56
PublicKeyCredentialCreationOptionsJSON,
6-
PublicKeyCredentialCreationOptionsWithoutExtensions,
77
SignUpVerificationJSON,
88
SignUpVerificationResource,
99
SignUpVerificationsJSON,
@@ -58,7 +58,7 @@ export class Verification extends BaseResource implements VerificationResource {
5858
}
5959

6060
export class PasskeyVerification extends Verification implements PasskeyVerificationResource {
61-
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions | null = null;
61+
publicKey: __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions | null = null;
6262

6363
constructor(data: VerificationJSON | null) {
6464
super(data);

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

+60-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import type { PublicKeyCredentialCreationOptionsJSON } from '@clerk/types';
1+
import type {
2+
type __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse,
3+
type __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
4+
PublicKeyCredentialCreationOptionsJSON,
5+
PublicKeyCredentialRequestOptionsJSON,
6+
} from '@clerk/types';
27

3-
import type { PublicKeyCredentialWithAuthenticatorAttestationResponse } from '../passkeys';
4-
import { bufferToBase64Url, convertJSONToPublicKeyCreateOptions, serializePublicKeyCredential } from '../passkeys';
8+
import {
9+
bufferToBase64Url,
10+
convertJSONToPublicKeyCreateOptions,
11+
convertJSONToPublicKeyRequestOptions,
12+
serializePublicKeyCredential,
13+
serializePublicKeyCredentialAssertion,
14+
} from '../passkeys';
515

616
describe('Passkey utils', () => {
717
describe('serialization', () => {
@@ -57,8 +67,29 @@ describe('Passkey utils', () => {
5767
expect(bufferToBase64Url(result.excludeCredentials[0].id)).toEqual(pkCreateOptions.excludeCredentials[0].id);
5868
});
5969

70+
it('convertJSONToPublicKeyCreateOptions()', () => {
71+
const pkCreateOptions: PublicKeyCredentialRequestOptionsJSON = {
72+
rpId: 'clerk.com',
73+
allowCredentials: [
74+
{
75+
type: 'public-key',
76+
id: 'cmFuZG9tX2lk',
77+
},
78+
],
79+
userVerification: 'required',
80+
timeout: 10000,
81+
challenge: 'Y2hhbGxlbmdlXzEyMw', // challenge_123 encoded as base64url
82+
};
83+
84+
const result = convertJSONToPublicKeyRequestOptions(pkCreateOptions);
85+
86+
expect(result.rpId).toEqual('clerk.com');
87+
expect(result.userVerification).toEqual('required');
88+
expect(bufferToBase64Url(result.allowCredentials[0].id)).toEqual(pkCreateOptions.allowCredentials[0].id);
89+
});
90+
6091
it('serializePublicKeyCredential()', () => {
61-
const publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse = {
92+
const publicKeyCredential: __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse = {
6293
type: 'public-key',
6394
id: 'credentialId_123',
6495
rawId: new Uint8Array([99, 114, 101, 100, 101, 110, 116, 105, 97, 108, 73, 100, 95, 49, 50, 51]),
@@ -80,5 +111,30 @@ describe('Passkey utils', () => {
80111
expect(result.response.attestationObject).toEqual('bElkXzE');
81112
expect(result.response.transports).toEqual(['usb']);
82113
});
114+
115+
it('serializePublicKeyCredentialAssertion()', () => {
116+
const publicKeyCredential: __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse = {
117+
type: 'public-key',
118+
id: 'credentialId_123',
119+
rawId: new Uint8Array([99, 114, 101, 100, 101, 110, 116, 105, 97, 108, 73, 100, 95, 49, 50, 51]),
120+
authenticatorAttachment: 'cross-platform',
121+
response: {
122+
clientDataJSON: new Uint8Array([110, 116, 105, 97]),
123+
signature: new Uint8Array([108, 73, 100, 95, 49]),
124+
authenticatorData: new Uint8Array([108, 73, 100, 95, 49]),
125+
userHandle: null,
126+
},
127+
};
128+
129+
const result = serializePublicKeyCredentialAssertion(publicKeyCredential);
130+
131+
expect(result.type).toEqual('public-key');
132+
expect(result.id).toEqual('credentialId_123');
133+
expect(result.rawId).toEqual('Y3JlZGVudGlhbElkXzEyMw');
134+
135+
expect(result.response.clientDataJSON).toEqual('bnRpYQ');
136+
expect(result.response.signature).toEqual('bElkXzE');
137+
expect(result.response.userHandle).toEqual(null);
138+
});
83139
});
84140
});

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

+106-23
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
import { isValidBrowser } from '@clerk/shared/browser';
22
import { ClerkRuntimeError } from '@clerk/shared/error';
33
import type {
4+
__experimental_PublicKeyCredentialCreationOptionsWithoutExtensions,
5+
__experimental_PublicKeyCredentialRequestOptionsWithoutExtensions,
6+
__experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse,
7+
__experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse,
48
PublicKeyCredentialCreationOptionsJSON,
5-
PublicKeyCredentialCreationOptionsWithoutExtensions,
9+
PublicKeyCredentialRequestOptionsJSON,
610
} from '@clerk/types';
711

8-
type PublicKeyCredentialWithAuthenticatorAttestationResponse = Omit<
9-
PublicKeyCredential,
10-
'response' | 'getClientExtensionResults'
11-
> & {
12-
response: Omit<AuthenticatorAttestationResponse, 'getAuthenticatorData' | 'getPublicKey' | 'getPublicKeyAlgorithm'>;
13-
};
14-
15-
type WebAuthnCreateCredentialReturn =
12+
type CredentialReturn<T> =
1613
| {
17-
publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse;
14+
publicKeyCredential: T;
1815
error: null;
1916
}
2017
| {
2118
publicKeyCredential: null;
2219
error: ClerkWebAuthnError | Error;
2320
};
2421

25-
type ClerkWebAuthnErrorCode = 'passkey_exists' | 'passkey_registration_cancelled' | 'passkey_credential_failed';
22+
type WebAuthnCreateCredentialReturn =
23+
CredentialReturn<__experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse>;
24+
type WebAuthnGetCredentialReturn =
25+
CredentialReturn<__experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse>;
26+
27+
type ClerkWebAuthnErrorCode =
28+
| 'passkey_exists'
29+
| 'passkey_registration_cancelled'
30+
| 'passkey_credential_create_failed'
31+
| 'passkey_credential_get_failed';
2632

2733
function isWebAuthnSupported() {
2834
return (
@@ -73,17 +79,19 @@ class Base64Converter {
7379
}
7480

7581
async function webAuthnCreateCredential(
76-
publicKeyOptions: PublicKeyCredentialCreationOptionsWithoutExtensions,
82+
publicKeyOptions: __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions,
7783
): Promise<WebAuthnCreateCredentialReturn> {
7884
try {
7985
// Typescript types are not aligned with the spec. These type assertions are required to comply with the spec.
8086
const credential = (await navigator.credentials.create({
8187
publicKey: publicKeyOptions,
82-
})) as PublicKeyCredentialWithAuthenticatorAttestationResponse | null;
88+
})) as __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse | null;
8389

8490
if (!credential) {
8591
return {
86-
error: new ClerkWebAuthnError('Browser failed to create credential', { code: 'passkey_credential_failed' }),
92+
error: new ClerkWebAuthnError('Browser failed to create credential', {
93+
code: 'passkey_credential_create_failed',
94+
}),
8795
publicKeyCredential: null,
8896
};
8997
}
@@ -94,6 +102,33 @@ async function webAuthnCreateCredential(
94102
}
95103
}
96104

105+
async function webAuthnGetCredential({
106+
publicKeyOptions,
107+
conditionalUI,
108+
}: {
109+
publicKeyOptions: __experimental_PublicKeyCredentialRequestOptionsWithoutExtensions;
110+
conditionalUI: boolean;
111+
}): Promise<WebAuthnGetCredentialReturn> {
112+
try {
113+
// Typescript types are not aligned with the spec. These type assertions are required to comply with the spec.
114+
const credential = (await navigator.credentials.get({
115+
publicKey: publicKeyOptions,
116+
mediation: conditionalUI ? 'conditional' : 'optional',
117+
})) as __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse | null;
118+
119+
if (!credential) {
120+
return {
121+
error: new ClerkWebAuthnError('Browser failed to get credential', { code: 'passkey_credential_get_failed' }),
122+
publicKeyCredential: null,
123+
};
124+
}
125+
126+
return { publicKeyCredential: credential, error: null };
127+
} catch (e) {
128+
return { error: handlePublicKeyGetError(e), publicKeyCredential: null };
129+
}
130+
}
131+
97132
/**
98133
* Map webauthn errors from `navigator.credentials.create()` to Clerk-js errors
99134
* @param error
@@ -107,6 +142,17 @@ function handlePublicKeyCreateError(error: Error): ClerkWebAuthnError | ClerkRun
107142
return error;
108143
}
109144

145+
/**
146+
* Map webauthn errors from `navigator.credentials.get()` to Clerk-js errors
147+
* @param error
148+
*/
149+
function handlePublicKeyGetError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
150+
if (error.name === 'NotAllowedError') {
151+
return new ClerkWebAuthnError(error.message, { code: 'passkey_registration_cancelled' });
152+
}
153+
return error;
154+
}
155+
110156
function convertJSONToPublicKeyCreateOptions(jsonPublicKey: PublicKeyCredentialCreationOptionsJSON) {
111157
const userIdBuffer = base64UrlToBuffer(jsonPublicKey.user.id);
112158
const challengeBuffer = base64UrlToBuffer(jsonPublicKey.challenge);
@@ -124,16 +170,37 @@ function convertJSONToPublicKeyCreateOptions(jsonPublicKey: PublicKeyCredentialC
124170
...jsonPublicKey.user,
125171
id: userIdBuffer,
126172
},
127-
} as PublicKeyCredentialCreationOptionsWithoutExtensions;
173+
} as __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions;
174+
}
175+
176+
function convertJSONToPublicKeyRequestOptions(jsonPublicKey: PublicKeyCredentialRequestOptionsJSON) {
177+
const challengeBuffer = base64UrlToBuffer(jsonPublicKey.challenge);
178+
179+
const allowCredentialsWithBuffer = (jsonPublicKey.allowCredentials || []).map(cred => ({
180+
...cred,
181+
id: base64UrlToBuffer(cred.id),
182+
}));
183+
184+
return {
185+
...jsonPublicKey,
186+
allowCredentials: allowCredentialsWithBuffer,
187+
challenge: challengeBuffer,
188+
} as __experimental_PublicKeyCredentialRequestOptionsWithoutExtensions;
128189
}
129190

130-
function serializePublicKeyCredential(publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse) {
131-
const response = publicKeyCredential.response;
191+
function __serializePublicKeyCredential<T extends Omit<PublicKeyCredential, 'getClientExtensionResults'>>(pkc: T) {
132192
return {
133-
type: publicKeyCredential.type,
134-
id: publicKeyCredential.id,
135-
rawId: bufferToBase64Url(publicKeyCredential.rawId),
136-
authenticatorAttachment: publicKeyCredential.authenticatorAttachment,
193+
type: pkc.type,
194+
id: pkc.id,
195+
rawId: bufferToBase64Url(pkc.rawId),
196+
authenticatorAttachment: pkc.authenticatorAttachment,
197+
};
198+
}
199+
200+
function serializePublicKeyCredential(pkc: __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse) {
201+
const response = pkc.response;
202+
return {
203+
...__serializePublicKeyCredential(pkc),
137204
response: {
138205
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
139206
attestationObject: bufferToBase64Url(response.attestationObject),
@@ -142,6 +209,21 @@ function serializePublicKeyCredential(publicKeyCredential: PublicKeyCredentialWi
142209
};
143210
}
144211

212+
function serializePublicKeyCredentialAssertion(
213+
pkc: __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse,
214+
) {
215+
const response = pkc.response;
216+
return {
217+
...__serializePublicKeyCredential(pkc),
218+
response: {
219+
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
220+
authenticatorData: bufferToBase64Url(response.authenticatorData),
221+
signature: bufferToBase64Url(response.signature),
222+
userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null,
223+
},
224+
};
225+
}
226+
145227
const bufferToBase64Url = Base64Converter.encode.bind(Base64Converter);
146228
const base64UrlToBuffer = Base64Converter.decode.bind(Base64Converter);
147229

@@ -165,8 +247,9 @@ export {
165247
bufferToBase64Url,
166248
handlePublicKeyCreateError,
167249
webAuthnCreateCredential,
250+
webAuthnGetCredential,
168251
convertJSONToPublicKeyCreateOptions,
252+
convertJSONToPublicKeyRequestOptions,
169253
serializePublicKeyCredential,
254+
serializePublicKeyCredentialAssertion,
170255
};
171-
172-
export type { PublicKeyCredentialWithAuthenticatorAttestationResponse };

‎packages/types/src/factors.ts

+21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import type { __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse } from './passkey';
12
import type {
3+
__experimental_PasskeyStrategy,
24
BackupCodeStrategy,
35
EmailCodeStrategy,
46
EmailLinkStrategy,
@@ -44,6 +46,13 @@ export type PasswordFactor = {
4446
strategy: PasswordStrategy;
4547
};
4648

49+
/**
50+
* @experimental
51+
*/
52+
export type __experimental_PasskeyFactor = {
53+
strategy: __experimental_PasskeyStrategy;
54+
};
55+
4756
export type OauthFactor = {
4857
strategy: OAuthStrategy;
4958
};
@@ -85,6 +94,10 @@ export type EmailLinkConfig = Omit<EmailLinkFactor, 'safeIdentifier'> & {
8594
};
8695
export type PhoneCodeConfig = Omit<PhoneCodeFactor, 'safeIdentifier'>;
8796
export type Web3SignatureConfig = Web3SignatureFactor;
97+
/**
98+
* @experimental
99+
*/
100+
export type __experimental_PassKeyConfig = __experimental_PasskeyFactor;
88101
export type OAuthConfig = OauthFactor & {
89102
redirectUrl: string;
90103
actionCompleteRedirectUrl: string;
@@ -115,6 +128,14 @@ export type PasswordAttempt = {
115128
password: string;
116129
};
117130

131+
/**
132+
* @experimental
133+
*/
134+
export type __experimental_PasskeyAttempt = {
135+
strategy: __experimental_PasskeyStrategy;
136+
publicKeyCredential: __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse;
137+
};
138+
118139
export type Web3Attempt = {
119140
strategy: Web3Strategy;
120141
signature: string;

‎packages/types/src/json.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ interface PublicKeyCredentialUserEntityJSON {
461461
id: Base64UrlString;
462462
}
463463

464-
export interface ExcludedCredentialJSON {
464+
interface PublicKeyCredentialDescriptorJSON {
465465
type: 'public-key';
466466
id: Base64UrlString;
467467
transports?: ('ble' | 'hybrid' | 'internal' | 'nfc' | 'usb')[];
@@ -479,11 +479,19 @@ export interface PublicKeyCredentialCreationOptionsJSON {
479479
challenge: Base64UrlString;
480480
pubKeyCredParams: PublicKeyCredentialParameters[];
481481
timeout: number;
482-
excludeCredentials: ExcludedCredentialJSON[];
482+
excludeCredentials: PublicKeyCredentialDescriptorJSON[];
483483
authenticatorSelection: AuthenticatorSelectionCriteriaJSON;
484484
attestation: 'direct' | 'enterprise' | 'indirect' | 'none';
485485
}
486486

487+
export interface PublicKeyCredentialRequestOptionsJSON {
488+
allowCredentials: PublicKeyCredentialDescriptorJSON[];
489+
challenge: Base64UrlString;
490+
rpId: string;
491+
timeout: number;
492+
userVerification: 'discouraged' | 'preferred' | 'required';
493+
}
494+
487495
// TODO-PASSKEYS: Decide if we are keeping this
488496
// export interface PassKeyVerificationJSON extends VerificationJSON {
489497
// publicKey: PublicKeyCredentialCreationOptionsJSON | null;

‎packages/types/src/passkey.ts

+33
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,36 @@ export interface PasskeyResource extends ClerkResource {
2020
update: (params: UpdatePasskeyParams) => Promise<PasskeyResource>;
2121
delete: () => Promise<DeletedObjectResource>;
2222
}
23+
24+
/**
25+
* @experimental
26+
*/
27+
export type __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions = Omit<
28+
Required<PublicKeyCredentialCreationOptions>,
29+
'extensions'
30+
>;
31+
/**
32+
* @experimental
33+
*/
34+
export type __experimental_PublicKeyCredentialRequestOptionsWithoutExtensions = Omit<
35+
Required<PublicKeyCredentialRequestOptions>,
36+
'extensions'
37+
>;
38+
/**
39+
* @experimental
40+
*/
41+
export type __experimental_PublicKeyCredentialWithAuthenticatorAttestationResponse = Omit<
42+
PublicKeyCredential,
43+
'response' | 'getClientExtensionResults'
44+
> & {
45+
response: Omit<AuthenticatorAttestationResponse, 'getAuthenticatorData' | 'getPublicKey' | 'getPublicKeyAlgorithm'>;
46+
};
47+
/**
48+
* @experimental
49+
*/
50+
export type __experimental_PublicKeyCredentialWithAuthenticatorAssertionResponse = Omit<
51+
PublicKeyCredential,
52+
'response' | 'getClientExtensionResults'
53+
> & {
54+
response: AuthenticatorAssertionResponse;
55+
};

‎packages/types/src/signIn.ts

+12
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export interface SignInResource extends ClerkResource {
9090

9191
authenticateWithMetamask: () => Promise<SignInResource>;
9292

93+
__experimental_authenticateWithPasskey: () => Promise<SignInResource>;
94+
9395
createEmailLinkFlow: () => CreateEmailLinkFlowReturn<SignInStartEmailLinkFlowParams, SignInResource>;
9496

9597
validatePassword: (password: string, callbacks?: ValidatePasswordCallbacks) => void;
@@ -113,6 +115,8 @@ export type SignInFirstFactor =
113115
| EmailLinkFactor
114116
| PhoneCodeFactor
115117
| PasswordFactor
118+
// TODO-PASSKEYS: Include this when the feature is not longer considered experimental
119+
// | __experimental_PasskeyFactor
116120
| ResetPasswordPhoneCodeFactor
117121
| ResetPasswordEmailCodeFactor
118122
| Web3SignatureFactor
@@ -135,12 +139,16 @@ export type PrepareFirstFactorParams =
135139
| EmailLinkConfig
136140
| PhoneCodeConfig
137141
| Web3SignatureConfig
142+
// TODO-PASSKEYS: Include this when the feature is not longer considered experimental
143+
// | __experimental_PassKeyConfig
138144
| ResetPasswordPhoneCodeFactorConfig
139145
| ResetPasswordEmailCodeFactorConfig
140146
| OAuthConfig
141147
| SamlConfig;
142148

143149
export type AttemptFirstFactorParams =
150+
// TODO-PASSKEYS: Include this when the feature is not longer considered experimental
151+
// | __experimental_PasskeyAttempt
144152
| EmailCodeAttempt
145153
| PhoneCodeAttempt
146154
| PasswordAttempt
@@ -168,6 +176,8 @@ export type SignInCreateParams = (
168176
password: string;
169177
identifier: string;
170178
}
179+
// TODO-PASSKEYS: Include this when the feature is not longer considered experimental
180+
// | { strategy: __experimental_PasskeyStrategy }
171181
| {
172182
strategy:
173183
| PhoneCodeStrategy
@@ -198,6 +208,8 @@ export interface SignInStartEmailLinkFlowParams extends StartEmailLinkFlowParams
198208
}
199209

200210
export type SignInStrategy =
211+
// TODO-PASSKEYS: Include this when the feature is not longer considered experimental
212+
// | __experimental_PasskeyStrategy
201213
| PasswordStrategy
202214
| ResetPasswordPhoneCodeStrategy
203215
| ResetPasswordEmailCodeStrategy

‎packages/types/src/strategies.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { OAuthProvider } from './oauth';
22
import type { Web3Provider } from './web3';
33

4+
/**
5+
* @experimental
6+
*/
7+
export type __experimental_PasskeyStrategy = 'passkey';
48
export type PasswordStrategy = 'password';
59
export type PhoneCodeStrategy = 'phone_code';
610
export type EmailCodeStrategy = 'email_code';

‎packages/types/src/verification.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ClerkAPIError } from './api';
2+
import type { __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions } from './passkey';
23
import type { ClerkResource } from './resource';
34

45
export interface VerificationResource extends ClerkResource {
@@ -13,12 +14,8 @@ export interface VerificationResource extends ClerkResource {
1314
verifiedFromTheSameClient: () => boolean;
1415
}
1516

16-
export type PublicKeyCredentialCreationOptionsWithoutExtensions = Omit<
17-
Required<PublicKeyCredentialCreationOptions>,
18-
'extensions'
19-
>;
2017
export interface PasskeyVerificationResource extends VerificationResource {
21-
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions | null;
18+
publicKey: __experimental_PublicKeyCredentialCreationOptionsWithoutExtensions | null;
2219
}
2320

2421
export type VerificationStatus = 'unverified' | 'verified' | 'transferable' | 'failed' | 'expired';

0 commit comments

Comments
 (0)
Please sign in to comment.