Skip to content

Commit fafa76f

Browse files
authoredMar 4, 2024··
feat(clerk-js): Adds experimental support for registering a passkey (#2884)
* feat(clerk-js): Adds experimental support for registering a passkey * chore(clerk-js): Add changelog * fix(clerk-js): Align endpoints and payload * chore(clerk-js): Support create endpoint for passkeys * test(clerk-js): Test transformations for webauthn payloads * chore(remix): Minor refactor * chore(clerk-js): Update experimental prefix * chore(clerk-js): Use nonce and convert it to publicKey - Remove prepare verification step - Update environment to handle passkey attribute * chore(clerk-js): Improve comments
1 parent 7c66796 commit fafa76f

File tree

14 files changed

+519
-2
lines changed

14 files changed

+519
-2
lines changed
 

‎.changeset/late-insects-doubt.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 a user to register a passkey for their account.
7+
Usage: `await clerk.user.__experimental__createPasskey()`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { PasskeyJSON, PasskeyResource, PasskeyVerificationResource } from '@clerk/types';
2+
3+
import { unixEpochToDate } from '../../utils/date';
4+
import type { PublicKeyCredentialWithAuthenticatorAttestationResponse } from '../../utils/passkeys';
5+
import {
6+
isWebAuthnPlatformAuthenticatorSupported,
7+
serializePublicKeyCredential,
8+
webAuthnCreateCredential,
9+
} from '../../utils/passkeys';
10+
import { BaseResource, ClerkRuntimeError, PasskeyVerification } from './internal';
11+
12+
export class Passkey extends BaseResource implements PasskeyResource {
13+
id!: string;
14+
pathRoot = '/me/passkeys';
15+
credentialId: string | null = null;
16+
verification: PasskeyVerificationResource | null = null;
17+
name: string | null = null;
18+
lastUsedAt: Date | null = null;
19+
createdAt!: Date;
20+
updatedAt!: Date;
21+
22+
public constructor(data: PasskeyJSON) {
23+
super();
24+
this.fromJSON(data);
25+
}
26+
27+
private static async create() {
28+
return BaseResource._fetch({
29+
path: `/me/passkeys`,
30+
method: 'POST',
31+
}).then(res => new Passkey(res?.response as PasskeyJSON));
32+
}
33+
34+
private static async attemptVerification(
35+
passkeyId: string,
36+
credential: PublicKeyCredentialWithAuthenticatorAttestationResponse,
37+
) {
38+
const jsonPublicKeyCredential = serializePublicKeyCredential(credential);
39+
return BaseResource._fetch({
40+
path: `/me/passkeys/${passkeyId}/attempt_verification`,
41+
method: 'POST',
42+
body: { strategy: 'passkey', publicKeyCredential: JSON.stringify(jsonPublicKeyCredential) } as any,
43+
}).then(res => new Passkey(res?.response as PasskeyJSON));
44+
}
45+
46+
/**
47+
* TODO-PASSKEYS: Implement this later
48+
*
49+
* GET /v1/me/passkeys
50+
*/
51+
static async get() {}
52+
53+
/**
54+
* Developers should not be able to create a new Passkeys from an already instanced object
55+
*/
56+
static async registerPasskey() {
57+
/**
58+
* The UI should always prevent from this method being called if WebAuthn is not supported.
59+
* As a precaution we need to check if WebAuthn is supported.
60+
*/
61+
62+
/**
63+
* TODO-PASSKEYS: First simply check if webauthn is supported and check for this only when
64+
* publicKey?.authenticatorSelection.authenticatorAttachment === 'platform'
65+
*/
66+
if (!(await isWebAuthnPlatformAuthenticatorSupported())) {
67+
throw new ClerkRuntimeError('Platform authenticator is not supported', {
68+
code: 'passkeys_unsupported_platform_authenticator',
69+
});
70+
}
71+
72+
const passkey = await this.create();
73+
74+
const { verification } = passkey;
75+
76+
const publicKey = verification?.publicKey;
77+
78+
// This should never occur such a fail-safe
79+
if (!publicKey) {
80+
// TODO-PASSKEYS: Implement this later
81+
throw 'Missing key';
82+
}
83+
84+
// Invoke the WebAuthn create() method.
85+
const { publicKeyCredential, error } = await webAuthnCreateCredential(publicKey);
86+
87+
if (!publicKeyCredential) {
88+
throw error;
89+
}
90+
91+
return this.attemptVerification(passkey.id, publicKeyCredential);
92+
}
93+
94+
/**
95+
* TODO-PASSKEYS: Implement this later
96+
*
97+
* PATCH /v1/me/passkeys/{passkeyIdentificationID}
98+
*/
99+
update = (): Promise<PasskeyResource> => this._basePatch();
100+
101+
/**
102+
* TODO-PASSKEYS: Implement this later
103+
*
104+
* DELETE /v1/me/passkeys/{passkeyIdentificationID}
105+
*/
106+
destroy = (): Promise<void> => this._baseDelete();
107+
108+
/**
109+
* TODO-PASSKEYS: Implement this later
110+
*
111+
* GET /v1/me/passkeys/{passkeyIdentificationID}
112+
*/
113+
reload = () => this._baseGet();
114+
115+
protected fromJSON(data: PasskeyJSON | null): this {
116+
if (!data) {
117+
return this;
118+
}
119+
120+
this.id = data.id;
121+
this.credentialId = data.credential_id;
122+
this.name = data.name;
123+
this.lastUsedAt = data.last_used_at ? unixEpochToDate(data.last_used_at) : null;
124+
this.createdAt = unixEpochToDate(data.created_at);
125+
this.updatedAt = unixEpochToDate(data.updated_at);
126+
127+
if (data.verification) {
128+
this.verification = new PasskeyVerification(data.verification);
129+
}
130+
return this;
131+
}
132+
}

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

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
GetUserOrganizationSuggestionsParams,
1616
ImageResource,
1717
OrganizationMembershipResource,
18+
PasskeyResource,
1819
PhoneNumberResource,
1920
RemoveUserPasswordParams,
2021
SamlAccountResource,
@@ -41,6 +42,7 @@ import {
4142
Image,
4243
OrganizationMembership,
4344
OrganizationSuggestion,
45+
Passkey,
4446
PhoneNumber,
4547
SamlAccount,
4648
SessionWithActivities,
@@ -124,6 +126,14 @@ export class User extends BaseResource implements UserResource {
124126
).create();
125127
};
126128

129+
/**
130+
* @experimental
131+
* This method is experimental, avoid using this in production applications
132+
*/
133+
__experimental_createPasskey = (): Promise<PasskeyResource> => {
134+
return Passkey.registerPasskey();
135+
};
136+
127137
createPhoneNumber = (params: CreatePhoneNumberParams): Promise<PhoneNumberResource> => {
128138
const { phoneNumber } = params || {};
129139
return new PhoneNumber(

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

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { parseError } from '@clerk/shared/error';
22
import type {
33
ClerkAPIError,
4+
PasskeyVerificationResource,
5+
PublicKeyCredentialCreationOptionsJSON,
6+
PublicKeyCredentialCreationOptionsWithoutExtensions,
47
SignUpVerificationJSON,
58
SignUpVerificationResource,
69
SignUpVerificationsJSON,
@@ -11,6 +14,7 @@ import type {
1114
} from '@clerk/types';
1215

1316
import { unixEpochToDate } from '../../utils/date';
17+
import { convertJSONToPublicKeyCreateOptions } from '../../utils/passkeys';
1418
import { BaseResource } from './internal';
1519

1620
export class Verification extends BaseResource implements VerificationResource {
@@ -53,6 +57,27 @@ export class Verification extends BaseResource implements VerificationResource {
5357
}
5458
}
5559

60+
export class PasskeyVerification extends Verification implements PasskeyVerificationResource {
61+
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions | null = null;
62+
63+
constructor(data: VerificationJSON | null) {
64+
super(data);
65+
this.fromJSON(data);
66+
}
67+
68+
/**
69+
* Transform base64url encoded strings to ArrayBuffer
70+
*/
71+
protected fromJSON(data: VerificationJSON | null): this {
72+
if (data?.nonce) {
73+
this.publicKey = convertJSONToPublicKeyCreateOptions(
74+
JSON.parse(data.nonce) as PublicKeyCredentialCreationOptionsJSON,
75+
);
76+
}
77+
return this;
78+
}
79+
}
80+
5681
export class SignUpVerifications implements SignUpVerificationsResource {
5782
emailAddress: SignUpVerificationResource;
5883
phoneNumber: SignUpVerificationResource;

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

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export * from './OrganizationMembershipRequest';
2020
export * from './OrganizationSuggestion';
2121
export * from './SamlAccount';
2222
export * from './Session';
23+
export * from './Passkey';
2324
export * from './PublicUserData';
2425
export * from './SessionWithActivities';
2526
export * from './SignIn';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { PublicKeyCredentialCreationOptionsJSON } from '@clerk/types';
2+
3+
import type { PublicKeyCredentialWithAuthenticatorAttestationResponse } from '../passkeys';
4+
import { bufferToBase64Url, convertJSONToPublicKeyCreateOptions, serializePublicKeyCredential } from '../passkeys';
5+
6+
describe('Passkey utils', () => {
7+
describe('serialization', () => {
8+
it('convertJSONToPublicKeyCreateOptions()', () => {
9+
const pkCreateOptions: PublicKeyCredentialCreationOptionsJSON = {
10+
rp: {
11+
name: 'clerk.com',
12+
id: 'clerk.com',
13+
},
14+
user: {
15+
name: 'clerkUser',
16+
displayName: 'Clerk User',
17+
id: 'dXNlcl8xMjM', // user_123 encoded as base64url
18+
},
19+
excludeCredentials: [
20+
{
21+
type: 'public-key',
22+
id: 'cmFuZG9tX2lk',
23+
},
24+
],
25+
authenticatorSelection: {
26+
requireResidentKey: true,
27+
residentKey: 'required',
28+
userVerification: 'required',
29+
},
30+
attestation: 'none',
31+
pubKeyCredParams: [
32+
{
33+
type: 'public-key',
34+
alg: -7,
35+
},
36+
],
37+
timeout: 10000,
38+
challenge: 'Y2hhbGxlbmdlXzEyMw', // challenge_123 encoded as base64url
39+
};
40+
41+
const result = convertJSONToPublicKeyCreateOptions(pkCreateOptions);
42+
43+
expect(result.rp).toEqual({
44+
name: 'clerk.com',
45+
id: 'clerk.com',
46+
});
47+
48+
expect(result.attestation).toEqual('none');
49+
expect(result.authenticatorSelection).toEqual({
50+
requireResidentKey: true,
51+
residentKey: 'required',
52+
userVerification: 'required',
53+
});
54+
55+
expect(bufferToBase64Url(result.user.id)).toEqual(pkCreateOptions.user.id);
56+
57+
expect(bufferToBase64Url(result.excludeCredentials[0].id)).toEqual(pkCreateOptions.excludeCredentials[0].id);
58+
});
59+
60+
it('serializePublicKeyCredential()', () => {
61+
const publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse = {
62+
type: 'public-key',
63+
id: 'credentialId_123',
64+
rawId: new Uint8Array([99, 114, 101, 100, 101, 110, 116, 105, 97, 108, 73, 100, 95, 49, 50, 51]),
65+
authenticatorAttachment: 'cross-platform',
66+
response: {
67+
clientDataJSON: new Uint8Array([110, 116, 105, 97]),
68+
attestationObject: new Uint8Array([108, 73, 100, 95, 49]),
69+
getTransports: () => ['usb'],
70+
},
71+
};
72+
73+
const result = serializePublicKeyCredential(publicKeyCredential);
74+
75+
expect(result.type).toEqual('public-key');
76+
expect(result.id).toEqual('credentialId_123');
77+
expect(result.rawId).toEqual('Y3JlZGVudGlhbElkXzEyMw');
78+
79+
expect(result.response.clientDataJSON).toEqual('bnRpYQ');
80+
expect(result.response.attestationObject).toEqual('bElkXzE');
81+
expect(result.response.transports).toEqual(['usb']);
82+
});
83+
});
84+
});
+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { isValidBrowser } from '@clerk/shared/browser';
2+
import { ClerkRuntimeError } from '@clerk/shared/error';
3+
import type {
4+
PublicKeyCredentialCreationOptionsJSON,
5+
PublicKeyCredentialCreationOptionsWithoutExtensions,
6+
} from '@clerk/types';
7+
8+
type PublicKeyCredentialWithAuthenticatorAttestationResponse = Omit<
9+
PublicKeyCredential,
10+
'response' | 'getClientExtensionResults'
11+
> & {
12+
response: Omit<AuthenticatorAttestationResponse, 'getAuthenticatorData' | 'getPublicKey' | 'getPublicKeyAlgorithm'>;
13+
};
14+
15+
type WebAuthnCreateCredentialReturn =
16+
| {
17+
publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse;
18+
error: null;
19+
}
20+
| {
21+
publicKeyCredential: null;
22+
error: ClerkWebAuthnError | Error;
23+
};
24+
25+
type ClerkWebAuthnErrorCode = 'passkey_exists' | 'passkey_registration_cancelled' | 'passkey_credential_failed';
26+
27+
function isWebAuthnSupported() {
28+
return (
29+
isValidBrowser() &&
30+
// Check if `PublicKeyCredential` is a constructor
31+
typeof window.PublicKeyCredential === 'function'
32+
);
33+
}
34+
35+
async function isWebAuthnAutofillSupported(): Promise<boolean> {
36+
try {
37+
return isWebAuthnSupported() && (await window.PublicKeyCredential.isConditionalMediationAvailable());
38+
} catch (e) {
39+
return false;
40+
}
41+
}
42+
43+
async function isWebAuthnPlatformAuthenticatorSupported(): Promise<boolean> {
44+
try {
45+
return (
46+
typeof window !== 'undefined' &&
47+
(await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())
48+
);
49+
} catch (e) {
50+
return false;
51+
}
52+
}
53+
54+
class Base64Converter {
55+
static encode(buffer: ArrayBuffer): string {
56+
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
57+
.replace(/\+/g, '-')
58+
.replace(/\//g, '_')
59+
.replace(/=+$/, '');
60+
}
61+
62+
static decode(base64url: string): ArrayBuffer {
63+
// TODO-PASSKEYS: check if this can be replaced with Buffer.from(base64url, 'base64');
64+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
65+
66+
const binaryString = atob(base64);
67+
const length = binaryString.length;
68+
const bytes = new Uint8Array(length);
69+
for (let i = 0; i < length; i++) {
70+
bytes[i] = binaryString.charCodeAt(i);
71+
}
72+
return bytes.buffer;
73+
}
74+
}
75+
76+
async function webAuthnCreateCredential(
77+
publicKeyOptions: PublicKeyCredentialCreationOptionsWithoutExtensions,
78+
): Promise<WebAuthnCreateCredentialReturn> {
79+
try {
80+
// Typescript types are not aligned with the spec. These type assertions are required to comply with the spec.
81+
const credential = (await navigator.credentials.create({
82+
publicKey: publicKeyOptions,
83+
})) as PublicKeyCredential | null;
84+
85+
if (!credential) {
86+
return {
87+
error: new ClerkWebAuthnError('Browser failed to create credential', { code: 'passkey_credential_failed' }),
88+
publicKeyCredential: null,
89+
};
90+
}
91+
92+
// Typescript types are not aligned with the spec. These type assertions are required to comply with the spec.
93+
const res = credential.response as AuthenticatorAttestationResponse;
94+
95+
return { publicKeyCredential: { ...credential, response: res }, error: null };
96+
} catch (e) {
97+
return { error: handlePublicKeyCreateError(e), publicKeyCredential: null };
98+
}
99+
}
100+
101+
/**
102+
* Map webauthn errors from `navigator.credentials.create()` to Clerk-js errors
103+
* @param error
104+
*/
105+
function handlePublicKeyCreateError(error: Error): ClerkWebAuthnError | ClerkRuntimeError | Error {
106+
if (error.name === 'InvalidStateError') {
107+
return new ClerkWebAuthnError(error.message, { code: 'passkey_exists' });
108+
} else if (error.name === 'NotAllowedError') {
109+
return new ClerkWebAuthnError(error.message, { code: 'passkey_registration_cancelled' });
110+
}
111+
return error;
112+
}
113+
114+
function convertJSONToPublicKeyCreateOptions(jsonPublicKey: PublicKeyCredentialCreationOptionsJSON) {
115+
const userIdBuffer = base64UrlToBuffer(jsonPublicKey.user.id);
116+
const challengeBuffer = base64UrlToBuffer(jsonPublicKey.challenge);
117+
118+
const excludeCredentialsWithBuffer = (jsonPublicKey.excludeCredentials || []).map(cred => ({
119+
...cred,
120+
id: base64UrlToBuffer(cred.id),
121+
}));
122+
123+
return {
124+
...jsonPublicKey,
125+
excludeCredentials: excludeCredentialsWithBuffer,
126+
challenge: challengeBuffer,
127+
user: {
128+
...jsonPublicKey.user,
129+
id: userIdBuffer,
130+
},
131+
} as PublicKeyCredentialCreationOptionsWithoutExtensions;
132+
}
133+
134+
function serializePublicKeyCredential(publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse) {
135+
const response = publicKeyCredential.response;
136+
return {
137+
type: publicKeyCredential.type,
138+
id: publicKeyCredential.id,
139+
rawId: bufferToBase64Url(publicKeyCredential.rawId),
140+
authenticatorAttachment: publicKeyCredential.authenticatorAttachment,
141+
response: {
142+
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
143+
attestationObject: bufferToBase64Url(response.attestationObject),
144+
transports: response.getTransports(),
145+
},
146+
};
147+
}
148+
149+
const bufferToBase64Url = Base64Converter.encode.bind(Base64Converter);
150+
const base64UrlToBuffer = Base64Converter.decode.bind(Base64Converter);
151+
152+
export class ClerkWebAuthnError extends ClerkRuntimeError {
153+
/**
154+
* A unique code identifying the error, can be used for localization.
155+
*/
156+
code: ClerkWebAuthnErrorCode;
157+
158+
constructor(message: string, { code }: { code: ClerkWebAuthnErrorCode }) {
159+
super(message, { code });
160+
this.code = code;
161+
}
162+
}
163+
164+
export {
165+
isWebAuthnPlatformAuthenticatorSupported,
166+
isWebAuthnAutofillSupported,
167+
isWebAuthnSupported,
168+
base64UrlToBuffer,
169+
bufferToBase64Url,
170+
handlePublicKeyCreateError,
171+
webAuthnCreateCredential,
172+
convertJSONToPublicKeyCreateOptions,
173+
serializePublicKeyCredential,
174+
};
175+
176+
export type { PublicKeyCredentialWithAuthenticatorAttestationResponse };

‎packages/shared/src/error.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class ClerkRuntimeError extends Error {
139139
message: string;
140140

141141
/**
142-
* A unique code identifying the error, used for localization
142+
* A unique code identifying the error, can be used for localization.
143143
*
144144
* @type {string}
145145
* @memberof ClerkRuntimeError

‎packages/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ export * from './web3';
5454
export * from './web3Wallet';
5555
export * from './customPages';
5656
export * from './pagination';
57+
export * from './passkey';

‎packages/types/src/json.ts

+51
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ export interface PhoneNumberJSON extends ClerkResourceJSON {
136136
backup_codes?: string[];
137137
}
138138

139+
export interface PasskeyJSON extends ClerkResourceJSON {
140+
object: 'passkey';
141+
id: string;
142+
credential_id: string | null;
143+
name: string | null;
144+
verification: VerificationJSON | null;
145+
last_used_at: number | null;
146+
updated_at: number;
147+
created_at: number;
148+
}
149+
139150
export interface Web3WalletJSON extends ClerkResourceJSON {
140151
object: 'web3_wallet';
141152
id: string;
@@ -437,3 +448,43 @@ export interface DeletedObjectJSON {
437448

438449
export type SignInFirstFactorJSON = CamelToSnake<SignInFirstFactor>;
439450
export type SignInSecondFactorJSON = CamelToSnake<SignInSecondFactor>;
451+
452+
/**
453+
* Types for WebAuthN passkeys
454+
*/
455+
456+
type Base64UrlString = string;
457+
458+
interface PublicKeyCredentialUserEntityJSON {
459+
name: string;
460+
displayName: string;
461+
id: Base64UrlString;
462+
}
463+
464+
export interface ExcludedCredentialJSON {
465+
type: 'public-key';
466+
id: Base64UrlString;
467+
transports?: ('ble' | 'hybrid' | 'internal' | 'nfc' | 'usb')[];
468+
}
469+
470+
interface AuthenticatorSelectionCriteriaJSON {
471+
requireResidentKey: boolean;
472+
residentKey: 'discouraged' | 'preferred' | 'required';
473+
userVerification: 'discouraged' | 'preferred' | 'required';
474+
}
475+
476+
export interface PublicKeyCredentialCreationOptionsJSON {
477+
rp: PublicKeyCredentialRpEntity;
478+
user: PublicKeyCredentialUserEntityJSON;
479+
challenge: Base64UrlString;
480+
pubKeyCredParams: PublicKeyCredentialParameters[];
481+
timeout: number;
482+
excludeCredentials: ExcludedCredentialJSON[];
483+
authenticatorSelection: AuthenticatorSelectionCriteriaJSON;
484+
attestation: 'direct' | 'enterprise' | 'indirect' | 'none';
485+
}
486+
487+
// TODO-PASSKEYS: Decide if we are keeping this
488+
// export interface PassKeyVerificationJSON extends VerificationJSON {
489+
// publicKey: PublicKeyCredentialCreationOptionsJSON | null;
490+
// }

‎packages/types/src/passkey.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ClerkResource } from './resource';
2+
import type { PasskeyVerificationResource } from './verification';
3+
4+
export interface PublicKeyOptions extends PublicKeyCredentialCreationOptions {}
5+
6+
export interface PasskeyResource extends ClerkResource {
7+
id: string;
8+
credentialId: string | null;
9+
name: string | null;
10+
verification: PasskeyVerificationResource | null;
11+
lastUsedAt: Date | null;
12+
updatedAt: Date;
13+
createdAt: Date;
14+
}

‎packages/types/src/user.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { OrganizationInvitationStatus } from './organizationInvitation';
99
import type { OrganizationMembershipResource } from './organizationMembership';
1010
import type { OrganizationSuggestionResource, OrganizationSuggestionStatus } from './organizationSuggestion';
1111
import type { ClerkPaginatedResponse, ClerkPaginationParams } from './pagination';
12+
import type { PasskeyResource } from './passkey';
1213
import type { PhoneNumberResource } from './phoneNumber';
1314
import type { ClerkResource } from './resource';
1415
import type { SamlAccountResource } from './samlAccount';
@@ -88,6 +89,12 @@ export interface UserResource extends ClerkResource {
8889
updatePassword: (params: UpdateUserPasswordParams) => Promise<UserResource>;
8990
removePassword: (params: RemoveUserPasswordParams) => Promise<UserResource>;
9091
createEmailAddress: (params: CreateEmailAddressParams) => Promise<EmailAddressResource>;
92+
93+
/**
94+
* @experimental
95+
* This method is experimental, avoid using this in production applications
96+
*/
97+
__experimental_createPasskey: () => Promise<PasskeyResource>;
9198
createPhoneNumber: (params: CreatePhoneNumberParams) => Promise<PhoneNumberResource>;
9299
createWeb3Wallet: (params: CreateWeb3WalletParams) => Promise<Web3WalletResource>;
93100
isPrimaryIdentification: (ident: EmailAddressResource | PhoneNumberResource | Web3WalletResource) => boolean;

‎packages/types/src/userSettings.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export type Attribute =
1111
| 'password'
1212
| 'web3_wallet'
1313
| 'authenticator_app'
14-
| 'backup_code';
14+
| 'backup_code'
15+
| 'passkey';
1516

1617
export type VerificationStrategy = 'email_link' | 'email_code' | 'phone_code' | 'totp' | 'backup_code';
1718

‎packages/types/src/verification.ts

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export interface VerificationResource extends ClerkResource {
1313
verifiedFromTheSameClient: () => boolean;
1414
}
1515

16+
export type PublicKeyCredentialCreationOptionsWithoutExtensions = Omit<
17+
Required<PublicKeyCredentialCreationOptions>,
18+
'extensions'
19+
>;
20+
export interface PasskeyVerificationResource extends VerificationResource {
21+
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions | null;
22+
}
23+
1624
export type VerificationStatus = 'unverified' | 'verified' | 'transferable' | 'failed' | 'expired';
1725

1826
export interface CodeVerificationAttemptParam {

0 commit comments

Comments
 (0)
Please sign in to comment.