Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create PKCE session for verification with email #907

Merged
merged 2 commits into from Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/auth-core/src/core.ts
Expand Up @@ -211,11 +211,14 @@ export class Auth {
}

async resendVerificationEmailForEmail(email: string, verifyUrl: string) {
const { verifier, challenge } = await pkce.createVerifierChallengePair();
await this._post("resend-verification-email", {
provider: emailPasswordProviderName,
email,
verify_url: verifyUrl,
challenge,
});
return { verifier };
}

async sendPasswordResetEmail(email: string, resetUrl: string) {
Expand Down
58 changes: 40 additions & 18 deletions packages/auth-express/src/index.ts
Expand Up @@ -516,24 +516,46 @@ 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",
});
} else {
throw new InvalidDataError(
"verification_token or email missing from request body"
);
}
res.status(204);
next();
} catch (err) {
next(err);
}
},
};
}

Expand Down
52 changes: 28 additions & 24 deletions packages/auth-nextjs/src/app/index.ts
Expand Up @@ -145,37 +145,41 @@ export class NextAppAuth extends NextAuth {
emailPasswordResendVerificationEmail: async (
data: FormData | { verification_token: string } | { email: string }
) => {
let email;
let verificationToken;
try {
[verificationToken] = _extractParams(
data,
["verification_token"],
"verification_token missing"
);
} catch (tokenError) {
try {
[email] = _extractParams(data, ["email"], "email missing");
} catch (emailError) {
const bothParamsMissing = [tokenError, emailError]
.map((err) => (err as Error).message)
.join(" and ");

throw new InvalidDataError(
`${bothParamsMissing}. Either one is required.`
);
}
}
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) {
await (await this.core).resendVerificationEmail(verificationToken);
} else if (email) {
await (
await this.core
).resendVerificationEmail(verificationToken.toString());
} else if (email) {
const { verifier } = await (
await this.core
).resendVerificationEmailForEmail(
email,
email.toString(),
`${this._authRoute}/emailpassword/verify`
);

cookies().set({
name: this.options.pkceVerifierCookieName,
value: verifier,
httpOnly: true,
sameSite: "strict",
});
} else {
throw new InvalidDataError(
"either verification_token or email must be provided"
);
}
},
};
Expand Down
5 changes: 5 additions & 0 deletions packages/auth-nextjs/src/pages/client.ts
Expand Up @@ -54,6 +54,11 @@ export class NextPagesClientAuth extends NextAuthHelpers {
| {
verification_token: string;
}
| {
email: string;
verify_url: string;
challenge: string;
}
| FormData
) {
return await apiRequest(
Expand Down
51 changes: 41 additions & 10 deletions packages/auth-nextjs/src/shared.ts
Expand Up @@ -506,15 +506,44 @@ 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 });
const verificationToken =
data instanceof FormData
? data.get("verification_token")?.toString()
: data.verification_token;
const email =
data instanceof FormData
? data.get("email")?.toString()
: data.email;

if (verificationToken) {
await (
await this.core
).resendVerificationEmail(verificationToken.toString());
return isAction
? Response.json({ _data: null })
: new Response(null, { status: 204 });
} else if (email) {
const { verifier } = await (
await this.core
).resendVerificationEmailForEmail(
email.toString(),
`${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(
"verification_token or email missing from request body"
);
}
}
default:
return new Response("Unknown auth route", {
Expand Down Expand Up @@ -550,7 +579,9 @@ export class NextAuthSession {
}
}

function _getReqBody(req: NextRequest) {
function _getReqBody(
req: NextRequest
): Promise<FormData | Record<string, unknown>> {
return req.headers.get("Content-Type") === "application/json"
? req.json()
: req.formData();
Expand Down
43 changes: 34 additions & 9 deletions packages/auth-remix/src/server.ts
Expand Up @@ -496,21 +496,22 @@ export class RemixServerAuth extends RemixClientAuth {

async emailPasswordResendVerificationEmail(
req: Request,
data?: { verification_token: string }
data?: { verification_token: string } | { email: string }
): Promise<{ headers: Headers }>;
async emailPasswordResendVerificationEmail<Res>(
req: Request,
cb: (error?: Error) => Res | Promise<Res>
): Promise<Res extends Response ? Res : TypedResponse<Res>>;
async emailPasswordResendVerificationEmail<Res>(
req: Request,
data: { verification_token: string },
data: { verification_token: string } | { email: string },
cb: (error?: Error) => Res | Promise<Res>
): Promise<Res extends Response ? Res : TypedResponse<Res>>;
async emailPasswordResendVerificationEmail<Res>(
req: Request,
dataOrCb?:
| { verification_token: string }
| { email: string }
| ((error?: Error) => Res | Promise<Res>),
cb?: (error?: Error) => Res | Promise<Res>
): Promise<
Expand All @@ -520,14 +521,38 @@ export class RemixServerAuth extends RemixClientAuth {
| (Res extends Response ? Res : TypedResponse<Res>)
> {
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,
Expand Down
42 changes: 35 additions & 7 deletions packages/auth-sveltekit/src/server.ts
Expand Up @@ -172,15 +172,43 @@ export class ServerRequestAuth extends ClientAuth {
}

async emailPasswordResendVerificationEmail(
data: { verification_token: string } | FormData
data: { verification_token: string } | { email: string } | FormData
): Promise<void> {
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(
Expand Down