diff --git a/.github/assets/passkeys.png b/.github/assets/passkeys.png
new file mode 100644
index 000000000..8142bc64a
Binary files /dev/null and b/.github/assets/passkeys.png differ
diff --git a/README.md b/README.md
index b2f589cac..21907f004 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,7 @@ To get notified first when we add new features, please subscribe to [our newslet
|
Multi-tenancy & teams
Manage B2B customers with an organization structure that makes sense and scales to millions. |
|
| Role-based access control
Define an arbitrary permission graph and assign it to users. Organizations can create org-specific roles. |
|
| OAuth Connections
Beyond login, Stack can also manage access tokens for third-party APIs, such as Outlook and Google Calendar. It handles refreshing tokens and controlling scope, making access tokens accessible via a single function call. |
|
+| Passkeys
Support for passwordless authentication using passkeys, allowing users to sign in securely with biometrics or security keys across all their devices. |
|
| Impersonation
Impersonate users for debugging and support, logging into their account as if you were them. |
|
| Webhooks
Get notified when users use your product, built on Svix. |
|
| Automatic emails
Send customizable emails on triggers such as sign-up, password reset, and email verification, editable with a WYSIWYG editor. |
|
diff --git a/packages/stack/src/components-page/account-settings.tsx b/packages/stack/src/components-page/account-settings.tsx
index 2610d42eb..221abed6e 100644
--- a/packages/stack/src/components-page/account-settings.tsx
+++ b/packages/stack/src/components-page/account-settings.tsx
@@ -371,7 +371,7 @@ function usePasskeySection() {
const handleAddNewPasskey = async () => {
- await stackApp.registerPasskey();
+ await user.registerPasskey();
};
return (
diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts
index 2332df0b2..ce87ceeca 100644
--- a/packages/stack/src/lib/stack-app.ts
+++ b/packages/stack/src/lib/stack-app.ts
@@ -803,6 +803,41 @@ class _StackClientAppImpl> {
+
+ const initiationResult = await app._interface.initiatePasskeyRegistration({}, session);
+
+ if (initiationResult.status !== "ok") {
+ return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to get initiation options for passkey registration"));
+ }
+
+ const {options_json, code} = initiationResult.data;
+
+ // HACK: Override the rpID to be the actual domain
+ if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
+ throw new StackAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
+ }
+ options_json.rp.id = window.location.hostname;
+
+ let attResp;
+ try {
+ attResp = await startRegistration({ optionsJSON: options_json });
+ debugger;
+ } catch (error: any) {
+ if (error instanceof WebAuthnError) {
+ return Result.error(new KnownErrors.PasskeyWebAuthnError(error.message, error.name));
+ } else {
+ // This should never happen
+ return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to start passkey registration"));
+ }
+ }
+
+
+ const registrationResult = await app._interface.registerPasskey({ credential: attResp, code }, session);
+
+ await app._refreshUser(session);
+ return registrationResult;
+ },
signOut() {
return app._signOut(session);
},
@@ -1384,42 +1419,6 @@ class _StackClientAppImpl> {
- const session = this._getSession();
- const initiationResult = await this._interface.initiatePasskeyRegistration({}, session);
-
- if (initiationResult.status !== "ok") {
- return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to get initiation options for passkey registration"));
- }
-
- const {options_json, code} = initiationResult.data;
-
- // HACK: Override the rpID to be the actual domain
- if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
- throw new StackAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
- }
- options_json.rp.id = window.location.hostname;
-
- let attResp;
- try {
- attResp = await startRegistration({ optionsJSON: options_json });
- debugger;
- } catch (error: any) {
- if (error instanceof WebAuthnError) {
- return Result.error(new KnownErrors.PasskeyWebAuthnError(error.message, error.name));
- } else {
- // This should never happen
- return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to start passkey registration"));
- }
- }
-
-
- const registrationResult = await this._interface.registerPasskey({ credential: attResp, code }, session);
-
- await this._refreshUser(session);
- return registrationResult;
- }
-
async callOAuthCallback() {
this._ensurePersistentTokenStore();
@@ -2527,6 +2526,7 @@ type Auth = {
* ```
*/
getAuthJson(): Promise<{ accessToken: string | null, refreshToken: string | null }>,
+ registerPasskey(): Promise>,
};
/**
@@ -3161,7 +3161,6 @@ export type StackClientApp>,
verifyEmail(code: string): Promise>,
signInWithMagicLink(code: string): Promise>,
- registerPasskey(): Promise>,
redirectToOAuthCallback(): Promise,
useUser(options: GetUserOptions & { or: 'redirect' }): ProjectCurrentUser,