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. | Selected team switcher component | |

Role-based access control

Define an arbitrary permission graph and assign it to users. Organizations can create org-specific roles. | RBAC | |

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. | OAuth tokens | +|

Passkeys

Support for passwordless authentication using passkeys, allowing users to sign in securely with biometrics or security keys across all their devices. | OAuth tokens | |

Impersonation

Impersonate users for debugging and support, logging into their account as if you were them. | Webhooks | |

Webhooks

Get notified when users use your product, built on Svix. | Webhooks | |

Automatic emails

Send customizable emails on triggers such as sign-up, password reset, and email verification, editable with a WYSIWYG editor. | Email templates | 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,