Skip to content

fix: suppress verification emails for placeholder sessions in auth flow#1669

Open
paustint wants to merge 3 commits intomainfrom
fix/placeholder-email-sending
Open

fix: suppress verification emails for placeholder sessions in auth flow#1669
paustint wants to merge 3 commits intomainfrom
fix/placeholder-email-sending

Conversation

@paustint
Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI review requested due to automatic review settings April 14, 2026 15:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR suppresses verification emails for placeholder/temporary sessions created during the auth flow (notably when a registration attempt uses an already-registered email), to avoid spamming the actual inbox owner while keeping the overall verification redirect behavior consistent.

Changes:

  • Add guards in callback and resendVerification to skip sending verification emails/codes when the session is marked temporary / placeholder.
  • Add Vitest regression coverage to ensure placeholder sessions do not trigger email sends, while legitimate registrations/sessions still do.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
apps/api/src/app/controllers/auth.controller.ts Adds placeholder-session guards to suppress verification emails in callback and resendVerification.
apps/api/src/app/controllers/tests/auth.controller.spec.ts Adds regression tests verifying email suppression for placeholder sessions and normal behavior for real users.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/api/src/app/controllers/auth.controller.ts Outdated
Comment thread apps/api/src/app/controllers/auth.controller.ts Outdated
@paustint
Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

@copilot apply changes based on the comments in this thread

Applied both changes from that review thread in commit 20fd6e4:

  1. Comment wording (resendVerification, lines 872-873): Updated to reference "the callback handler for credentials login/register" instead of the ambiguous "login handler above".

  2. Timing side-channel (callback, lines 617-627): Changed await sendEmailVerification/sendVerificationCode to fire-and-forget (void ... .catch(error => ...)) so both the placeholder and non-placeholder paths return with comparable latency, removing the potential timing side-channel. Updated the explanatory comment accordingly. Also primed the test mocks to return resolved promises (required since .catch() is now called on the return value).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/api/src/app/controllers/auth.controller.ts Outdated
Comment thread apps/api/src/app/controllers/auth.controller.ts Outdated
Comment on lines 875 to 890

if (!isPlaceholderUser) {
switch (type) {
case 'email': {
await sendEmailVerification(req.session.user.email, token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
break;
}
case '2fa-email': {
await sendVerificationCode(req.session.user.email, token, TOKEN_DURATION_MINUTES);
break;
}
default: {
break;
}
}
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resendVerification now returns much faster for placeholder sessions (no email sent) than for real-user sessions (awaits email send). Since placeholder sessions are used specifically to prevent email enumeration, this latency difference can become a timing side-channel to distinguish “email already registered” vs “new user”. Consider using the same non-awaited/background dispatch pattern here as in the callback handler (and logging failures) so both placeholder and non-placeholder branches return with comparable latency.

Suggested change
if (!isPlaceholderUser) {
switch (type) {
case 'email': {
await sendEmailVerification(req.session.user.email, token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
break;
}
case '2fa-email': {
await sendVerificationCode(req.session.user.email, token, TOKEN_DURATION_MINUTES);
break;
}
default: {
break;
}
}
}
const resendVerificationEmailPromise = isPlaceholderUser
? Promise.resolve()
: (() => {
switch (type) {
case 'email': {
return sendEmailVerification(req.session.user.email, token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
}
case '2fa-email': {
return sendVerificationCode(req.session.user.email, token, TOKEN_DURATION_MINUTES);
}
default: {
return Promise.resolve();
}
}
})();
void resendVerificationEmailPromise.catch((error) => {
console.error('Failed to resend verification email.', getExceptionLog(error));
});

Copilot uses AI. Check for mistakes.
Comment thread apps/api/src/app/controllers/__tests__/auth.controller.spec.ts
@paustint
Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 15, 2026

@copilot apply changes based on the comments in this thread

Applied all 4 changes from that review thread in commit 6fd0169:

  1. callback reverted to await — email delivery failures now propagate through route error handling again (no silent redirect to /auth/verify on send failure). The placeholder-session guard is preserved; only the fire-and-forget + console.error approach was reverted.

  2. resendVerification now fire-and-forget — uses an IIFE ternary pattern (resendVerificationEmailPromise) so both placeholder (resolves immediately) and non-placeholder (dispatches email) branches return with comparable latency. Failures are logged via res.log.error with structured userId context.

  3. 2fa-email test coverage added — 4 new test cases covering the 2fa-email branch in both callback and resendVerification: placeholder suppression and real-user sends for each handler.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +609 to 622
// Suppress verification emails for placeholder sessions (e.g. registration with an already-registered email).
// The verify flow for a placeholder user is a dead-end that destroys the session, so the email would be
// pointless and would spam the real owner of the inbox. For non-placeholder users, await delivery so
// failures continue to surface through the existing route error handling instead of redirecting to
// /auth/verify when no verification message was sent.
const isPlaceholderUser = !!req.session.sessionDetails?.isTemporary || req.session.user.id === PLACEHOLDER_USER_ID;

if (!isPlaceholderUser) {
if (initialVerification.type === 'email') {
await sendEmailVerification(req.session.user.email, initialVerification.token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
} else if (initialVerification.type === '2fa-email') {
await sendVerificationCode(req.session.user.email, initialVerification.token, TOKEN_DURATION_MINUTES);
}
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In placeholder sessions you now skip sending the verification message, but the /auth/verification handler still requires a correct code/token before it reaches the placeholder-user branch that destroys the session and redirects with InvalidRegistration. With emails suppressed (and resend also suppressed), users will be redirected to /auth/verify with no way to obtain a valid token, leaving them stuck on the verify screen and potentially keeping temporary sessions alive until expiry. Consider either short-circuiting placeholder sessions before redirecting to /auth/verify (while preserving enumeration defense), or updating the verification flow to immediately destroy placeholder sessions without requiring a token.

Copilot uses AI. Check for mistakes.
})();

void resendVerificationEmailPromise.catch((error) => {
res.log.error({ ...getExceptionLog(error), userId: req.session.user.id }, 'Failed to resend verification email');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch-log message says "Failed to resend verification email", but this route can also resend a 2FA email code (type: '2fa-email'). Consider wording the log message more generically (e.g., "verification message") and/or include the type in the log fields so failures are easier to diagnose.

Suggested change
res.log.error({ ...getExceptionLog(error), userId: req.session.user.id }, 'Failed to resend verification email');
res.log.error(
{ ...getExceptionLog(error), userId: req.session.user.id, type },
'Failed to resend verification message'
);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants