Improved passkey UX (#325)

This commit is contained in:
Moritz Schneider 2024-10-29 17:21:07 -07:00 committed by GitHub
parent 93a1fcf7cb
commit 2650e92156
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 38 additions and 38 deletions

BIN
.github/assets/passkeys.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -64,6 +64,7 @@ To get notified first when we add new features, please subscribe to [our newslet
| <h3>Multi-tenancy & teams</h3> Manage B2B customers with an organization structure that makes sense and scales to millions. | <img alt="Selected team switcher component" src=".github/assets/team-switcher.png" width="400px"> |
| <h3>Role-based access control</h3> Define an arbitrary permission graph and assign it to users. Organizations can create org-specific roles. | <img alt="RBAC" src=".github/assets/permissions.png" width="400px"> |
| <h3>OAuth Connections</h3>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. | <img alt="OAuth tokens" src=".github/assets/connected-accounts.png" width="250px"> |
| <h3>Passkeys</h3> Support for passwordless authentication using passkeys, allowing users to sign in securely with biometrics or security keys across all their devices. | <img alt="OAuth tokens" src=".github/assets/passkeys.png" width="400px"> |
| <h3>Impersonation</h3> Impersonate users for debugging and support, logging into their account as if you were them. | <img alt="Webhooks" src=".github/assets/impersonate.png" width="350px"> |
| <h3>Webhooks</h3> Get notified when users use your product, built on Svix. | <img alt="Webhooks" src=".github/assets/stack-webhooks.png" width="300px"> |
| <h3>Automatic emails</h3> Send customizable emails on triggers such as sign-up, password reset, and email verification, editable with a WYSIWYG editor. | <img alt="Email templates" src=".github/assets/email-editor.png" width="400px"> |

View File

@ -371,7 +371,7 @@ function usePasskeySection() {
const handleAddNewPasskey = async () => {
await stackApp.registerPasskey();
await user.registerPasskey();
};
return (

View File

@ -803,6 +803,41 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
const tokens = await this.currentSession.getTokens();
return tokens;
},
async registerPasskey(): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
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<HasTokenStore extends boolean, ProjectId extends strin
}
}
async registerPasskey(): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
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<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>>,
};
/**
@ -3161,7 +3161,6 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
getTeamInvitationDetails(code: string): Promise<Result<{ teamDisplayName: string }, KnownErrors["VerificationCodeError"]>>,
verifyEmail(code: string): Promise<Result<undefined, KnownErrors["VerificationCodeError"]>>,
signInWithMagicLink(code: string): Promise<Result<undefined, KnownErrors["VerificationCodeError"] | KnownErrors["InvalidTotpCode"]>>,
registerPasskey(): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>>,
redirectToOAuthCallback(): Promise<void>,
useUser(options: GetUserOptions<HasTokenStore> & { or: 'redirect' }): ProjectCurrentUser<ProjectId>,