🏷️ fix: Categorize Auth Tokens by Flow Type (#13556)

* fix: Scope auth token lifecycle

* fix: Preserve legacy auth token lookup

* fix: Scope verification token cleanup
This commit is contained in:
Danny Avila 2026-06-06 14:22:06 -04:00 committed by GitHub
parent 5011be4d38
commit 3571dfcf22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 545 additions and 49 deletions

View file

@ -45,9 +45,112 @@ const domains = {
server: process.env.DOMAIN_SERVER,
};
const AuthTokenTypes = Object.freeze({
EMAIL_VERIFICATION: 'email_verification',
PASSWORD_RESET: 'password_reset',
});
const latestAuthTokenOptions = Object.freeze({ sort: { createdAt: -1 } });
const genericVerificationMessage = 'Please check your email to verify your email address.';
const OPENID_SESSION_ID_TOKEN_EXPIRY_BUFFER_SECONDS = 30;
const findPasswordResetToken = async (userId) => {
const typedToken = await findToken(
{
userId,
type: AuthTokenTypes.PASSWORD_RESET,
},
latestAuthTokenOptions,
);
if (typedToken) {
return typedToken;
}
return await findToken(
{
userId,
email: null,
identifier: null,
type: null,
},
latestAuthTokenOptions,
);
};
const findEmailVerificationToken = async (user) => {
const typedToken = await findToken(
{
userId: user._id,
email: user.email,
type: AuthTokenTypes.EMAIL_VERIFICATION,
},
latestAuthTokenOptions,
);
if (typedToken) {
return typedToken;
}
return await findToken(
{
userId: user._id,
email: user.email,
identifier: null,
type: null,
},
latestAuthTokenOptions,
);
};
const deleteEmailVerificationTokens = (user) =>
Promise.all([
deleteTokens({
userId: user._id,
email: user.email,
type: AuthTokenTypes.EMAIL_VERIFICATION,
}),
deleteTokens({
userId: user._id,
email: user.email,
identifier: null,
type: null,
}),
]);
const getEmailVerificationTokenDeleteQuery = (emailVerificationToken) => {
if (!emailVerificationToken.identifier && !emailVerificationToken.type) {
return {
token: emailVerificationToken.token,
userId: emailVerificationToken.userId,
email: emailVerificationToken.email,
identifier: null,
type: null,
};
}
return {
token: emailVerificationToken.token,
type: AuthTokenTypes.EMAIL_VERIFICATION,
};
};
const getPasswordResetTokenDeleteQuery = (passwordResetToken) => {
if (!passwordResetToken.email && !passwordResetToken.type) {
return {
token: passwordResetToken.token,
email: null,
identifier: null,
type: null,
};
}
return {
token: passwordResetToken.token,
type: AuthTokenTypes.PASSWORD_RESET,
};
};
const getUnexpiredOpenIDSessionIdToken = (idToken) => {
if (!idToken) {
return;
@ -133,6 +236,7 @@ const sendVerificationEmail = async (user) => {
await createToken({
userId: user._id,
email: user.email,
type: AuthTokenTypes.EMAIL_VERIFICATION,
token: hash,
createdAt: Date.now(),
expiresIn: 900,
@ -161,11 +265,11 @@ const verifyEmail = async (req) => {
return { message: 'Email already verified', status: 'success' };
}
let emailVerificationData = await findToken({ email: decodedEmail }, { sort: { createdAt: -1 } });
const emailVerificationData = await findEmailVerificationToken(user);
if (!emailVerificationData) {
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`);
return new Error('Invalid or expired password reset token');
return new Error('Invalid or expired email verification token');
}
const isValid = bcrypt.compareSync(token, emailVerificationData.token);
@ -184,7 +288,7 @@ const verifyEmail = async (req) => {
return new Error('Failed to update user verification status');
}
await deleteTokens({ token: emailVerificationData.token });
await deleteTokens(getEmailVerificationTokenDeleteQuery(emailVerificationData));
logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`);
return { message: 'Email verification was successful', status: 'success' };
};
@ -337,12 +441,16 @@ const requestPasswordReset = async (req) => {
};
}
await deleteTokens({ userId: user._id });
await Promise.all([
deleteTokens({ userId: user._id, type: AuthTokenTypes.PASSWORD_RESET }),
deleteTokens({ userId: user._id, email: null, identifier: null, type: null }),
]);
const [resetToken, hash] = createTokenHash();
await createToken({
userId: user._id,
type: AuthTokenTypes.PASSWORD_RESET,
token: hash,
createdAt: Date.now(),
expiresIn: 900,
@ -386,12 +494,7 @@ const requestPasswordReset = async (req) => {
* @returns
*/
const resetPassword = async (userId, token, password) => {
let passwordResetToken = await findToken(
{
userId,
},
{ sort: { createdAt: -1 } },
);
const passwordResetToken = await findPasswordResetToken(userId);
if (!passwordResetToken) {
return new Error('Invalid or expired password reset token');
@ -419,7 +522,7 @@ const resetPassword = async (userId, token, password) => {
});
}
await deleteTokens({ token: passwordResetToken.token });
await deleteTokens(getPasswordResetTokenDeleteQuery(passwordResetToken));
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
return { message: 'Password reset was successful' };
};
@ -724,7 +827,6 @@ const setOpenIDAuthTokens = (
const resendVerificationEmail = async (req) => {
try {
const { email } = req.body;
await deleteTokens({ email });
const user = await findUser({ email }, 'email _id name');
if (!user) {
@ -732,6 +834,8 @@ const resendVerificationEmail = async (req) => {
return { status: 200, message: genericVerificationMessage };
}
await deleteEmailVerificationTokens(user);
const [verifyToken, hash] = createTokenHash();
const verificationLink = `${
@ -753,6 +857,7 @@ const resendVerificationEmail = async (req) => {
await createToken({
userId: user._id,
email: user.email,
type: AuthTokenTypes.EMAIL_VERIFICATION,
token: hash,
createdAt: Date.now(),
expiresIn: 900,