From 9bc04e1e1e691d74cf3df654094ba77a2995fd91 Mon Sep 17 00:00:00 2001 From: Scott Trinh Date: Wed, 13 Mar 2024 10:08:11 -0400 Subject: [PATCH] Set verifier in cookie for email-only verification --- packages/auth-express/src/index.ts | 54 ++++++++++++++++-------- packages/auth-nextjs/src/app/index.ts | 9 +++- packages/auth-nextjs/src/pages/client.ts | 5 +++ packages/auth-nextjs/src/shared.ts | 52 ++++++++++++++++++----- packages/auth-remix/src/server.ts | 38 ++++++++++++++--- packages/auth-sveltekit/src/server.ts | 42 +++++++++++++++--- 6 files changed, 157 insertions(+), 43 deletions(-) diff --git a/packages/auth-express/src/index.ts b/packages/auth-express/src/index.ts index 864c53ba8..7ff36ea74 100644 --- a/packages/auth-express/src/index.ts +++ b/packages/auth-express/src/index.ts @@ -516,24 +516,42 @@ export class ExpressAuth { next(err); } }, - resendVerificationEmail: async ( - req: AuthRequest, - res: ExpressResponse, - next: NextFunction - ) => { - try { - const [verificationToken] = _extractParams( - req.body, - ["verification_token"], - "verification_token missing from request body" - ); - (await this.core).resendVerificationEmail(verificationToken); - res.status(204); - next(); - } catch (err) { - next(err); - } - }, + resendVerificationEmail: + (verifyUrl?: string) => + async (req: AuthRequest, res: ExpressResponse, next: NextFunction) => { + try { + if ("verification_token" in req.body) { + const verificationToken = req.body.verification_token; + if (typeof verificationToken !== "string") { + throw new InvalidDataError( + "expected 'verification_token' to be a string" + ); + } + await (await this.core).resendVerificationEmail(verificationToken); + } else if ("email" in req.body) { + const email = req.body.email; + if (typeof email !== "string") { + throw new InvalidDataError("expected 'email' to be a string"); + } + if (!verifyUrl) { + throw new InvalidDataError( + "verifyUrl is required when email is provided" + ); + } + const { verifier } = await ( + await this.core + ).resendVerificationEmailForEmail(email, verifyUrl); + res.cookie(this.options.pkceVerifierCookieName, verifier, { + httpOnly: true, + sameSite: "strict", + }); + } + res.status(204); + next(); + } catch (err) { + next(err); + } + }, }; } diff --git a/packages/auth-nextjs/src/app/index.ts b/packages/auth-nextjs/src/app/index.ts index ef33a9c75..bfcfdbd31 100644 --- a/packages/auth-nextjs/src/app/index.ts +++ b/packages/auth-nextjs/src/app/index.ts @@ -170,12 +170,19 @@ export class NextAppAuth extends NextAuth { if (verificationToken) { await (await this.core).resendVerificationEmail(verificationToken); } else if (email) { - await ( + const { verifier } = await ( await this.core ).resendVerificationEmailForEmail( email, `${this._authRoute}/emailpassword/verify` ); + + cookies().set({ + name: this.options.pkceVerifierCookieName, + value: verifier, + httpOnly: true, + sameSite: "strict", + }); } }, }; diff --git a/packages/auth-nextjs/src/pages/client.ts b/packages/auth-nextjs/src/pages/client.ts index 33bac5e70..cf62c37ff 100644 --- a/packages/auth-nextjs/src/pages/client.ts +++ b/packages/auth-nextjs/src/pages/client.ts @@ -54,6 +54,11 @@ export class NextPagesClientAuth extends NextAuthHelpers { | { verification_token: string; } + | { + email: string; + verify_url: string; + challenge: string; + } | FormData ) { return await apiRequest( diff --git a/packages/auth-nextjs/src/shared.ts b/packages/auth-nextjs/src/shared.ts index c164ca9ca..3deef5052 100644 --- a/packages/auth-nextjs/src/shared.ts +++ b/packages/auth-nextjs/src/shared.ts @@ -506,15 +506,45 @@ export abstract class NextAuth extends NextAuthHelpers { case "emailpassword/resend-verification-email": { const data = await _getReqBody(req); const isAction = _isAction(data); - const [verificationToken] = _extractParams( - data, - ["verification_token"], - "verification_token missing from request body" - ); - (await this.core).resendVerificationEmail(verificationToken); - return isAction - ? Response.json({ _data: null }) - : new Response(null, { status: 204 }); + if ("verification_token" in data) { + const verificationToken = data.verification_token; + if (typeof verificationToken !== "string") { + throw new InvalidDataError( + "'verification_token' must be a string" + ); + } + await ( + await this.core + ).resendVerificationEmail(verificationToken); + return isAction + ? Response.json({ _data: null }) + : new Response(null, { status: 204 }); + } else if ("email" in data) { + const email = data.email; + if (typeof email !== "string") { + throw new InvalidDataError("'email' must be a string"); + } + const { verifier } = await ( + await this.core + ).resendVerificationEmailForEmail( + email, + `${this._authRoute}/emailpassword/verify` + ); + cookies().set({ + name: this.options.pkceVerifierCookieName, + value: verifier, + httpOnly: true, + sameSite: "strict", + path: "/", + }); + return isAction + ? Response.json({ _data: null }) + : new Response(null, { status: 204 }); + } else { + throw new InvalidDataError( + "Either verification_token or email is required" + ); + } } default: return new Response("Unknown auth route", { @@ -550,7 +580,9 @@ export class NextAuthSession { } } -function _getReqBody(req: NextRequest) { +function _getReqBody( + req: NextRequest +): Promise> { return req.headers.get("Content-Type") === "application/json" ? req.json() : req.formData(); diff --git a/packages/auth-remix/src/server.ts b/packages/auth-remix/src/server.ts index 3f5095323..49ecb2f88 100644 --- a/packages/auth-remix/src/server.ts +++ b/packages/auth-remix/src/server.ts @@ -520,14 +520,38 @@ export class RemixServerAuth extends RemixClientAuth { | (Res extends Response ? Res : TypedResponse) > { return handleAction( - async (data) => { - const [verificationToken] = _extractParams( - data, - ["verification_token"], - "verification_token missing" - ); + async (data, headers) => { + const verificationToken = + data instanceof FormData + ? data.get("verification_token") + : data.verification_token; + const email = data instanceof FormData ? data.get("email") : data.email; + + if (verificationToken) { + await ( + await this.core + ).resendVerificationEmail(verificationToken.toString()); + } else if (email) { + const { verifier } = await ( + await this.core + ).resendVerificationEmailForEmail( + email.toString(), + `${this._authRoute}/emailpassword/verify` + ); - await (await this.core).resendVerificationEmail(verificationToken); + headers.append( + "Set-Cookie", + cookie.serialize(this.options.pkceVerifierCookieName, verifier, { + httpOnly: true, + sameSite: "strict", + path: "/", + }) + ); + } else { + throw new InvalidDataError( + "verification_token or email missing. Either one is required." + ); + } }, req, dataOrCb, diff --git a/packages/auth-sveltekit/src/server.ts b/packages/auth-sveltekit/src/server.ts index 3ae94ab7e..9c73d527b 100644 --- a/packages/auth-sveltekit/src/server.ts +++ b/packages/auth-sveltekit/src/server.ts @@ -172,15 +172,43 @@ export class ServerRequestAuth extends ClientAuth { } async emailPasswordResendVerificationEmail( - data: { verification_token: string } | FormData + data: { verification_token: string } | { email: string } | FormData ): Promise { - const [verificationToken] = extractParams( - data, - ["verification_token"], - "verification_token missing" - ); + const verificationToken = + data instanceof FormData + ? data.get("verification_token") + : "verification_token" in data + ? data.verification_token + : null; + const email = + data instanceof FormData + ? data.get("email") + : "email" in data + ? data.email + : null; + + if (verificationToken) { + return await ( + await this.core + ).resendVerificationEmail(verificationToken.toString()); + } else if (email) { + const { verifier } = await ( + await this.core + ).resendVerificationEmailForEmail( + email.toString(), + `${this.config.authRoute}/emailpassword/verify` + ); - await (await this.core).resendVerificationEmail(verificationToken); + this.cookies.set(this.config.pkceVerifierCookieName, verifier, { + httpOnly: true, + sameSite: "strict", + path: "/", + }); + } else { + throw new InvalidDataError( + "expected 'verification_token' or 'email' in data" + ); + } } async emailPasswordSignIn(