fix: suppress verification emails for placeholder sessions in auth flow#1669
fix: suppress verification emails for placeholder sessions in auth flow#1669
Conversation
There was a problem hiding this comment.
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
callbackandresendVerificationto 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.
|
@copilot apply changes based on the comments in this thread |
…mment wording Agent-Logs-Url: https://github.com/jetstreamapp/jetstream/sessions/8600f048-7db8-4f29-aa6a-60ac22861223 Co-authored-by: paustint <5461649+paustint@users.noreply.github.com>
Applied both changes from that review thread in commit
|
There was a problem hiding this comment.
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.
|
|
||
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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 apply changes based on the comments in this thread |
…pression Agent-Logs-Url: https://github.com/jetstreamapp/jetstream/sessions/e9d5e767-aaeb-497c-ad2d-b5e0f7cc871d Co-authored-by: paustint <5461649+paustint@users.noreply.github.com>
Applied all 4 changes from that review thread in commit
|
There was a problem hiding this comment.
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.
| // 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| })(); | ||
|
|
||
| void resendVerificationEmailPromise.catch((error) => { | ||
| res.log.error({ ...getExceptionLog(error), userId: req.session.user.id }, 'Failed to resend verification email'); |
There was a problem hiding this comment.
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.
| 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' | |
| ); |
No description provided.