Skip to content

Commit

Permalink
Add Magic Link implementation to SvelteKit
Browse files Browse the repository at this point in the history
  • Loading branch information
scotttrinh committed Mar 21, 2024
1 parent 8ea7932 commit 2677fa4
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 31 deletions.
10 changes: 5 additions & 5 deletions packages/auth-sveltekit/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export interface AuthOptions {
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 @@ -19,12 +20,11 @@ export function getConfig(options: AuthOptions) {
options.authRoutesPath?.replace(/^\/|\/$/g, "") ?? "auth";

return {
authCookieName: "edgedb-session",
pkceVerifierCookieName: "edgedb-pkce-verifier",
...options,
baseUrl,
authRoutesPath,
authCookieName: options.authCookieName ?? "edgedb-session",
pkceVerifierCookieName:
options.pkceVerifierCookieName ?? "edgedb-pkce-verifier",
passwordResetPath: options.passwordResetPath,
authRoute: `${baseUrl}/${authRoutesPath}`,
};
}
Expand Down
156 changes: 130 additions & 26 deletions packages/auth-sveltekit/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export interface AuthRouteHandlers {
isSignUp: boolean;
}>
) => Promise<never>;
onMagicLinkCallback?: (
params: ParamsOrError<{ tokenData: TokenData; isSignUp: boolean }>
) => Promise<never>;
onBuiltinUICallback?: (
params: ParamsOrError<
(
Expand Down Expand Up @@ -153,21 +156,12 @@ export class ServerRequestAuth extends ClientAuth {
`${this.config.authRoute}/emailpassword/verify`
);

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

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

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

this.setAuthTokenCookie(tokenData.auth_token);
return { tokenData };
}

Expand Down Expand Up @@ -199,11 +193,7 @@ export class ServerRequestAuth extends ClientAuth {
await this.core
).signinWithEmailPassword(email, password);

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

return { tokenData };
}
Expand All @@ -224,11 +214,7 @@ export class ServerRequestAuth extends ClientAuth {
new URL(this.config.passwordResetPath, this.config.baseUrl).toString()
);

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

async emailPasswordResetPassword(
Expand All @@ -250,20 +236,82 @@ export class ServerRequestAuth extends ClientAuth {
await this.core
).resetPasswordWithResetToken(resetToken, verifier, password);

this.cookies.set(this.config.authCookieName, tokenData.auth_token, {
this.setAuthTokenCookie(tokenData.auth_token);
this.deleteVerifierCookie();
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 callbackUrl = new URL("magiclink/callback", this.config.authRoute);
callbackUrl.searchParams.set("isSignUp", "true");
const errorUrl = new URL(
this.config.magicLinkFailurePath,
this.config.baseUrl
);
const { verifier } = await (
await this.core
).signupWithMagicLink(email, callbackUrl.href, errorUrl.href);

this.setVerifierCookie(verifier);
}

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 callbackUrl = new URL("magiclink/callback", this.config.authRoute);
const errorUrl = new URL(
this.config.magicLinkFailurePath,
this.config.baseUrl
);
const { verifier } = await (
await this.core
).signinWithMagicLink(email, callbackUrl.href, errorUrl.href);

this.setVerifierCookie(verifier);
}

async signout(): Promise<void> {
this.deleteAuthTokenCookie();
}

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

private deleteVerifierCookie() {
this.cookies.delete(this.config.pkceVerifierCookieName, {
path: "/",
});
return { tokenData };
}

async signout(): Promise<void> {
this.cookies.delete(this.config.authCookieName, { path: "/" });
private setAuthTokenCookie(authToken: string) {
this.cookies.set(this.config.authCookieName, authToken, {
httpOnly: true,
sameSite: "strict",
path: "/",
});
}

private deleteAuthTokenCookie() {
this.cookies.delete(this.config.authCookieName, {
path: "/",
});
}
}

Expand Down Expand Up @@ -328,6 +376,7 @@ async function handleAuthRoutes(
onBuiltinUICallback,
onEmailVerify,
onSignout,
onMagicLinkCallback,
}: AuthRouteHandlers,
{ url, cookies }: RequestEvent,
core: Promise<Auth>,
Expand Down Expand Up @@ -423,6 +472,61 @@ 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 EdgeDBAuthError(error + (desc ? `: ${desc}` : "")),
});
}

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

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: "strict",
path: "/",
});

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

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

case "builtin/callback": {
if (!onBuiltinUICallback) {
throw new ConfigurationError(
Expand Down

0 comments on commit 2677fa4

Please sign in to comment.