From ebdb053403759a2de48a0f18d0ce545617ab5ca1 Mon Sep 17 00:00:00 2001 From: Scott Trinh Date: Thu, 21 Mar 2024 14:06:16 -0400 Subject: [PATCH] Add WebAuthn implementation to SvelteKit --- packages/auth-sveltekit/src/client.ts | 9 +++ packages/auth-sveltekit/src/server.ts | 101 ++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/packages/auth-sveltekit/src/client.ts b/packages/auth-sveltekit/src/client.ts index d5c6eb5dd..2ffca9329 100644 --- a/packages/auth-sveltekit/src/client.ts +++ b/packages/auth-sveltekit/src/client.ts @@ -1,4 +1,5 @@ import type { BuiltinOAuthProviderNames } from "@edgedb/auth-core"; +import { WebAuthnClient } from "@edgedb/auth-core/webauthn"; export interface AuthOptions { baseUrl: string; @@ -35,10 +36,18 @@ export default function createClientAuth(options: AuthOptions) { export class ClientAuth { protected readonly config: AuthConfig; + readonly webAuthnClient: WebAuthnClient; /** @internal */ constructor(options: AuthOptions) { this.config = getConfig(options); + this.webAuthnClient = new WebAuthnClient({ + signupOptionsUrl: `${this.config.authRoute}/webauthn/signup/options`, + signupUrl: `${this.config.authRoute}/webauthn/signup`, + signinOptionsUrl: `${this.config.authRoute}/webauthn/signin/options`, + signinUrl: `${this.config.authRoute}/webauthn/signin`, + verifyUrl: `${this.config.authRoute}/webauthn/verify`, + }); } getOAuthUrl(providerName: BuiltinOAuthProviderNames) { diff --git a/packages/auth-sveltekit/src/server.ts b/packages/auth-sveltekit/src/server.ts index cf8ea7288..d5a93afae 100644 --- a/packages/auth-sveltekit/src/server.ts +++ b/packages/auth-sveltekit/src/server.ts @@ -17,6 +17,8 @@ import { InvalidDataError, OAuthProviderFailureError, EdgeDBAuthError, + RegistrationResponseJSON, + AuthenticationResponseJSON, } from "@edgedb/auth-core"; import { ClientAuth, @@ -282,6 +284,45 @@ export class ServerRequestAuth extends ClientAuth { this.setVerifierCookie(verifier); } + async webAuthnSignUp(data: { + email: string; + credentials: RegistrationResponseJSON; + verify_url: string; + user_handle: string; + }): Promise<{ tokenData: TokenData | null }> { + const { + email, + credentials, + verify_url: verifyUrl, + user_handle: userHandle, + } = data; + + const result = await ( + await this.core + ).signupWithWebAuthn(email, credentials, verifyUrl, userHandle); + + this.setVerifierCookie(result.verifier); + if (result.status === "complete") { + this.setAuthTokenCookie(result.tokenData.auth_token); + return { tokenData: result.tokenData }; + } + + return { tokenData: null }; + } + + async webAuthnSignIn(data: { + email: string; + assertion: AuthenticationResponseJSON; + }): Promise<{ tokenData: TokenData | null }> { + const { email, assertion } = data; + const tokenData = await ( + await this.core + ).signinWithWebAuthn(email, assertion); + + this.setAuthTokenCookie(tokenData.auth_token); + return { tokenData }; + } + async signout(): Promise { this.deleteAuthTokenCookie(); } @@ -653,6 +694,66 @@ async function handleAuthRoutes( }); } + case "webauthn/signup/options": { + const email = searchParams.get("email"); + if (!email) { + throw new InvalidDataError("email missing"); + } + return redirect(302, (await core).getWebAuthnSignupOptionsUrl(email)); + } + + case "webauthn/signin/options": { + const email = searchParams.get("email"); + if (!email) { + throw new InvalidDataError("email missing"); + } + return redirect(302, (await core).getWebAuthnSigninOptionsUrl(email)); + } + + case "webauthn/verify": { + if (!onEmailVerify) { + throw new ConfigurationError( + `'onEmailVerify' auth route handler not configured` + ); + } + + const verificationToken = searchParams.get("verification_token"); + if (!verificationToken) { + return onEmailVerify({ + error: new InvalidDataError("verification_token missing"), + }); + } + const verifier = cookies.get(config.pkceVerifierCookieName); + if (!verifier) { + return onEmailVerify({ + error: new PKCEError("no pkce verifier cookie found"), + verificationToken, + }); + } + let tokenData: TokenData; + try { + tokenData = await ( + await core + ).verifyWebAuthnSignup(verificationToken, verifier); + } catch (err) { + return onEmailVerify({ + error: err instanceof Error ? err : new Error(String(err)), + verificationToken, + }); + } + + cookies.set(config.authCookieName, tokenData.auth_token, { + httpOnly: true, + sameSite: "strict", + path: "/", + }); + + return onEmailVerify({ + error: null, + tokenData, + }); + } + case "signout": { if (!onSignout) { throw new ConfigurationError(