fix: Ensure OTP is bound to workspace (#12096)

* fix: Ensure OTP is bound to teamId

* fix: Address review feedback on OTP tenant scoping

- Trim whitespace in VerificationCode Redis keys to match DB lookup
  normalization.
- Redirect with invalid-code (rather than leaking a backend error)
  when no user exists for the email in the resolved team.
- Correct retrieve() JSDoc to state undefined instead of null.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-04-17 23:22:58 -04:00
committed by GitHub
parent e49e3136b6
commit 347bdb10d4
4 changed files with 106 additions and 50 deletions
+16 -6
View File
@@ -15,6 +15,7 @@ import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { VerificationCode } from "@server/utils/VerificationCode";
import { signIn } from "@server/utils/authentication";
import { getUserForEmailSigninToken } from "@server/utils/jwt";
import { getTeamFromContext } from "@server/utils/passport";
import * as T from "./schema";
import { CSRF } from "@shared/constants";
@@ -128,28 +129,33 @@ const emailCallback = async (ctx: APIContext<T.EmailCallbackReq>) => {
return ctx.redirectOnClient(url.toString(), "POST");
}
let user!: User;
let user: User | null = null;
try {
if (token) {
user = await getUserForEmailSigninToken(ctx, token as string);
} else if (code && email) {
const team = await getTeamFromContext(ctx);
if (!team) {
ctx.redirect("/?notice=auth-error&description=Unknown%20team");
return;
}
user = await User.scope("withTeam").findOne({
rejectOnEmpty: true,
where: {
teamId: team.id,
email: email.trim().toLowerCase(),
},
});
const isValid = await VerificationCode.verify(email, code);
if (!isValid) {
if (!user || !(await VerificationCode.verify(team.id, email, code))) {
ctx.redirect(`/?notice=invalid-code`);
return;
}
// Delete the code after successful verification
await VerificationCode.delete(email);
await VerificationCode.delete(team.id, email);
} else {
ctx.redirect("/?notice=auth-error&description=Missing%20token");
return;
@@ -159,6 +165,10 @@ const emailCallback = async (ctx: APIContext<T.EmailCallbackReq>) => {
return ctx.redirect(`/?notice=auth-error&description=${err.message}`);
}
if (!user) {
return ctx.redirect(`/?notice=invalid-code`);
}
if (!user.team.emailSigninEnabled) {
return ctx.redirect(
"/?notice=auth-error&description=Disabled%20signin%20method"