Skip to content

Commit

Permalink
Add webauth & magic links to sveltekit
Browse files Browse the repository at this point in the history
  • Loading branch information
diksipav committed May 8, 2024
1 parent 994470f commit 06d1773
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 1 deletion.
13 changes: 12 additions & 1 deletion packages/auth-sveltekit/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { BuiltinOAuthProviderNames } from "@edgedb/auth-core";
import { WebAuthnClient } from "@edgedb/auth-core/webauthn";

export interface AuthOptions {
baseUrl: string;
authRoutesPath?: string;
authCookieName?: string;
pkceVerifierCookieName?: string;
passwordResetPath?: string;
magicLinkFailurePath?: string;
}

type OptionalOptions = "passwordResetPath";
type OptionalOptions = "passwordResetPath" | "magicLinkFailurePath";

export type AuthConfig = Required<Omit<AuthOptions, OptionalOptions>> &
Pick<AuthOptions, OptionalOptions> & { authRoute: string };
Expand All @@ -25,6 +27,7 @@ export function getConfig(options: AuthOptions) {
pkceVerifierCookieName:
options.pkceVerifierCookieName ?? "edgedb-pkce-verifier",
passwordResetPath: options.passwordResetPath,
magicLinkFailurePath: options.magicLinkFailurePath,
authRoute: `${baseUrl}/${authRoutesPath}`,
};
}
Expand All @@ -36,11 +39,19 @@ export default function createClientAuth(options: AuthOptions) {
export class ClientAuth {
protected readonly config: AuthConfig;
protected readonly isSecure: boolean;
readonly webAuthnClient: WebAuthnClient;

/** @internal */
constructor(options: AuthOptions) {
this.config = getConfig(options);
this.isSecure = this.config.baseUrl.startsWith("https");
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) {
Expand Down
226 changes: 226 additions & 0 deletions packages/auth-sveltekit/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
InvalidDataError,
OAuthProviderFailureError,
EdgeDBAuthError,
MagicLinkFailureError,
type AuthenticationResponseJSON,
type RegistrationResponseJSON,
} from "@edgedb/auth-core";
import {
ClientAuth,
Expand Down Expand Up @@ -64,6 +67,12 @@ export interface AuthRouteHandlers {
{ verificationToken?: string }
>
) => Promise<never>;
onMagicLinkCallback(
params: ParamsOrError<{
tokenData: TokenData;
isSignUp: boolean;
}>
): Promise<Response>;
onSignout?: () => Promise<never>;
}

Expand Down Expand Up @@ -288,6 +297,104 @@ export class ServerRequestAuth extends ClientAuth {
return { tokenData };
}

async magicLinkSignUp(data: { email: string } | FormData): Promise<void> {
if (!this.config.magicLinkFailurePath) {
throw new ConfigurationError(
`'magicLinkFailurePath' option not configured`
);
}
const [email] = extractParams(data, ["email"], "email missing");

const { verifier } = await (
await this.core
).signupWithMagicLink(
email,
`${this.config.authRoute}/magiclink/callback?isSignUp=true`,
new URL(this.config.magicLinkFailurePath, this.config.baseUrl).toString()
);

this.cookies.set(this.config.pkceVerifierCookieName, verifier, {
httpOnly: true,
sameSite: "strict",
path: "/",
});
}

async magicLinkSend(data: { email: string } | FormData): Promise<void> {
if (!this.config.magicLinkFailurePath) {
throw new ConfigurationError(
`'magicLinkFailurePath' option not configured`
);
}
const [email] = extractParams(data, ["email"], "email missing");

const { verifier } = await (
await this.core
).signinWithMagicLink(
email,
`${this.config.authRoute}/magiclink/callback?isSignUp=true`,
new URL(this.config.magicLinkFailurePath, this.config.baseUrl).toString()
);

this.cookies.set(this.config.pkceVerifierCookieName, verifier, {
httpOnly: true,
sameSite: "strict",
path: "/",
});
}

async webAuthnSignIn(data: {
email: string;
assertion: AuthenticationResponseJSON;
}): Promise<{ tokenData: TokenData }> {
const { email, assertion } = data;

const tokenData = await (
await this.core
).signinWithWebAuthn(email, assertion);

this.cookies.set(this.config.authCookieName, tokenData.auth_token, {
httpOnly: true,
sameSite: "strict",
path: "/",
});

return { tokenData };
}

async webAuthnSignUp(data: {
email: string;
credentials: RegistrationResponseJSON;
verify_url: string;
user_handle: string;
}): Promise<{ tokenData: TokenData | null }> {
const { email, credentials, verify_url, user_handle } = data;

const result = await (
await this.core
).signupWithWebAuthn(email, credentials, verify_url, user_handle);

this.cookies.set(this.config.pkceVerifierCookieName, result.verifier, {
httpOnly: true,
sameSite: "strict",
path: "/",
});

if (result.status === "complete") {
const tokenData = result.tokenData;

this.cookies.set(this.config.authCookieName, tokenData.auth_token, {
httpOnly: true,
sameSite: "strict",
path: "/",
});

return { tokenData };
}

return { tokenData: null };
}

async signout(): Promise<void> {
this.cookies.delete(this.config.authCookieName, { path: "/" });
}
Expand Down Expand Up @@ -353,6 +460,7 @@ async function handleAuthRoutes(
onOAuthCallback,
onBuiltinUICallback,
onEmailVerify,
onMagicLinkCallback,
onSignout,
}: AuthRouteHandlers,
{ url, cookies }: RequestEvent,
Expand Down Expand Up @@ -575,6 +683,124 @@ async function handleAuthRoutes(
});
}

case "magiclink/callback": {
if (!onMagicLinkCallback) {
throw new ConfigurationError(
`'onMagicLinkCallback' auth route handler not configured`
);
}

const error = searchParams.get("error");
if (error) {
const desc = searchParams.get("error_description");
return onMagicLinkCallback({
error: new MagicLinkFailureError(error + (desc ? `: ${desc}` : "")),
});
}

const code = searchParams.get("code");
const isSignUp = searchParams.get("isSignUp") === "true";
const verifier = cookies.get(config.pkceVerifierCookieName);
if (!code) {
return onMagicLinkCallback({
error: new PKCEError("no pkce code in response"),
});
}

if (!verifier) {
return onMagicLinkCallback({
error: new PKCEError("no pkce verifier cookie found"),
});
}

let tokenData: TokenData;
try {
tokenData = await (await core).getToken(code, verifier);
} catch (err) {
return onMagicLinkCallback({
error: err instanceof Error ? err : new Error(String(err)),
});
}

cookies.set(config.authCookieName, tokenData.auth_token, {
httpOnly: true,
sameSite: "lax",
path: "/",
});

cookies.set(config.pkceVerifierCookieName, "", {
maxAge: 0,
path: "/",
});

return onMagicLinkCallback({
error: null,
tokenData,
isSignUp,
});
}

case "webauthn/signup/options": {
const email = searchParams.get("email");
if (!email) {
throw new InvalidDataError("email missing");
}

return redirect(303, (await core).getWebAuthnSignupOptionsUrl(email));
}

case "webauthn/signin/options": {
const email = searchParams.get("email");
if (!email) {
throw new InvalidDataError("email missing");
}

return redirect(303, (await core).getWebAuthnSigninOptionsUrl(email));
}

case "webauthn/verify": {
if (!onEmailVerify) {
throw new ConfigurationError(
`'onEmailVerify' auth route handler not configured`
);
}
const verificationToken = searchParams.get("verification_token");
const verifier = cookies.get(config.pkceVerifierCookieName);
if (!verificationToken) {
return onEmailVerify({
error: new PKCEError("no verification_token in response"),
});
}
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(
Expand Down

0 comments on commit 06d1773

Please sign in to comment.