Skip to content

Commit e4e7990

Browse files
committed
feat(clerk-js,types): Add signals support for passkeys
1 parent f516a7e commit e4e7990

File tree

3 files changed

+98
-2
lines changed

3 files changed

+98
-2
lines changed

.changeset/honest-insects-deny.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
[Experimental] Add support for sign-in with passkey to new APIs

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
SignInFutureEmailLinkSendParams,
3939
SignInFutureFinalizeParams,
4040
SignInFutureMFAPhoneCodeVerifyParams,
41+
SignInFuturePasskeyParams,
4142
SignInFuturePasswordParams,
4243
SignInFuturePhoneCodeSendParams,
4344
SignInFuturePhoneCodeVerifyParams,
@@ -979,6 +980,77 @@ class SignInFuture implements SignInFutureResource {
979980
});
980981
}
981982

983+
async passkey(params?: SignInFuturePasskeyParams): Promise<{ error: unknown }> {
984+
const { flow } = params || {};
985+
986+
/**
987+
* The UI should always prevent from this method being called if WebAuthn is not supported.
988+
* As a precaution we need to check if WebAuthn is supported.
989+
*/
990+
991+
const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow;
992+
const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow;
993+
const isWebAuthnAutofillSupported =
994+
SignIn.clerk.__internal_isWebAuthnAutofillSupported || isWebAuthnAutofillSupportedOnWindow;
995+
996+
if (!isWebAuthnSupported()) {
997+
throw new ClerkWebAuthnError('Passkeys are not supported', {
998+
code: 'passkey_not_supported',
999+
});
1000+
}
1001+
1002+
return runAsyncResourceTask(this.resource, async () => {
1003+
if (flow === 'autofill' || flow === 'discoverable') {
1004+
await this._create({ strategy: 'passkey' });
1005+
} else {
1006+
const passKeyFactor = this.supportedFirstFactors.find(f => f.strategy === 'passkey') as PasskeyFactor;
1007+
1008+
if (!passKeyFactor) {
1009+
clerkVerifyPasskeyCalledBeforeCreate();
1010+
}
1011+
await this.resource.__internal_basePost({
1012+
body: { strategy: 'passkey' },
1013+
action: 'prepare_first_factor',
1014+
});
1015+
}
1016+
1017+
const { nonce } = this.firstFactorVerification;
1018+
const publicKeyOptions = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null;
1019+
1020+
if (!publicKeyOptions) {
1021+
clerkMissingWebAuthnPublicKeyOptions('get');
1022+
}
1023+
1024+
let canUseConditionalUI = false;
1025+
1026+
if (flow === 'autofill') {
1027+
/**
1028+
* If autofill is not supported gracefully handle the result, we don't need to throw.
1029+
* The caller should always check this before calling this method.
1030+
*/
1031+
canUseConditionalUI = await isWebAuthnAutofillSupported();
1032+
}
1033+
1034+
// Invoke the navigator.create.get() method.
1035+
const { publicKeyCredential, error } = await webAuthnGetCredential({
1036+
publicKeyOptions,
1037+
conditionalUI: canUseConditionalUI,
1038+
});
1039+
1040+
if (!publicKeyCredential) {
1041+
throw error;
1042+
}
1043+
1044+
await this.resource.__internal_basePost({
1045+
body: {
1046+
publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(publicKeyCredential)),
1047+
strategy: 'passkey',
1048+
},
1049+
action: 'attempt_first_factor',
1050+
});
1051+
});
1052+
}
1053+
9821054
async sendMFAPhoneCode(): Promise<{ error: unknown }> {
9831055
return runAsyncResourceTask(this.resource, async () => {
9841056
const phoneCodeFactor = this.resource.supportedSecondFactors?.find(f => f.strategy === 'phone_code');

packages/types/src/signInFuture.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { SetActiveNavigate } from './clerk';
22
import type { PhoneCodeChannel } from './phoneCodeChannel';
33
import type { SignInFirstFactor, SignInSecondFactor, SignInStatus, UserData } from './signInCommon';
4-
import type { OAuthStrategy, Web3Strategy } from './strategies';
4+
import type { OAuthStrategy, PasskeyStrategy, Web3Strategy } from './strategies';
55
import type { VerificationResource } from './verification';
66

77
export interface SignInFutureCreateParams {
@@ -14,7 +14,7 @@ export interface SignInFutureCreateParams {
1414
* The first factor verification strategy to use in the sign-in flow. Depends on the `identifier` value. Each
1515
* authentication identifier supports different verification strategies.
1616
*/
17-
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
17+
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso' | PasskeyStrategy;
1818
/**
1919
* The full URL or path that the OAuth provider should redirect to after successful authorization on their part.
2020
*/
@@ -215,6 +215,16 @@ export interface SignInFutureWeb3Params {
215215
strategy: Web3Strategy;
216216
}
217217

218+
export interface SignInFuturePasskeyParams {
219+
/**
220+
* The flow to use for the passkey sign-in.
221+
*
222+
* - `'autofill'`: The client prompts your users to select a passkey before they interact with your app.
223+
* - `'discoverable'`: The client requires the user to interact with the client.
224+
*/
225+
flow?: 'autofill' | 'discoverable';
226+
}
227+
218228
export interface SignInFutureFinalizeParams {
219229
navigate?: SetActiveNavigate;
220230
}
@@ -432,6 +442,14 @@ export interface SignInFutureResource {
432442
*/
433443
web3: (params: SignInFutureWeb3Params) => Promise<{ error: unknown }>;
434444

445+
/**
446+
* Initiates a passkey-based authentication flow, enabling users to authenticate using a previously
447+
* registered passkey. When called without parameters, this method requires a prior call to
448+
* `SignIn.create({ strategy: 'passkey' })` to initialize the sign-in context. This pattern is particularly useful in
449+
* scenarios where the authentication strategy needs to be determined dynamically at runtime.
450+
*/
451+
passkey: (params?: SignInFuturePasskeyParams) => Promise<{ error: unknown }>;
452+
435453
/**
436454
* Used to convert a sign-in with `status === 'complete'` into an active session. Will cause anything observing the
437455
* session state (such as the `useUser()` hook) to update automatically.

0 commit comments

Comments
 (0)