mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Address comments
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload");
|
||||
|
||||
if (!hasPayloadCol) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.text("payload").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload");
|
||||
|
||||
if (hasPayloadCol) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.dropColumn("payload");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ export const AuthTokensSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
aliasId: z.string().nullable().optional()
|
||||
aliasId: z.string().nullable().optional(),
|
||||
payload: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
||||
@@ -131,7 +131,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/me/email/otp",
|
||||
url: "/me/email-change/otp",
|
||||
config: {
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => req.permission.id
|
||||
@@ -167,7 +167,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
body: z.object({
|
||||
newEmail: z.string().email().trim(),
|
||||
otpCode: z.string().trim().length(8)
|
||||
otpCode: z.string().trim().length(6)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -37,9 +37,9 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
return { token, triesLeft, expiresAt };
|
||||
}
|
||||
case TokenType.TOKEN_EMAIL_CHANGE_OTP: {
|
||||
const token = String(crypto.randomInt(10 ** 7, 10 ** 8 - 1));
|
||||
const triesLeft = 3;
|
||||
const expiresAt = new Date(new Date().getTime() + 600000); // 10 minutes expiry
|
||||
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||
const triesLeft = 1;
|
||||
const expiresAt = new Date(new Date().getTime() + 600000);
|
||||
return { token, triesLeft, expiresAt };
|
||||
}
|
||||
case TokenType.TOKEN_EMAIL_MFA: {
|
||||
@@ -81,7 +81,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
||||
@@ -95,7 +95,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
userId,
|
||||
orgId,
|
||||
triesLeft: tkCfg?.triesLeft,
|
||||
aliasId
|
||||
aliasId,
|
||||
payload
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ export type TCreateTokenForUserDTO = {
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
aliasId?: string;
|
||||
payload?: string;
|
||||
};
|
||||
|
||||
export type TCreateOrgInviteTokenDTO = {
|
||||
|
||||
@@ -217,17 +217,18 @@ export const userServiceFactory = ({
|
||||
const existingUsers = await userDAL.findUserByUsername(newEmail.toLowerCase(), tx);
|
||||
const existingUser = existingUsers?.find((u) => u.id !== userId);
|
||||
if (existingUser) {
|
||||
// Don't reveal that email is taken - just don't send OTP
|
||||
// Frontend will show generic "check your email" message
|
||||
// Don't reveal that email is taken - just don't send OTP.
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
return { success: true, message: "Verification code sent to new email address" };
|
||||
}
|
||||
|
||||
// Generate 8-digit OTP and store newEmail in aliasId field temporarily
|
||||
// Generate 6-digit OTP
|
||||
const otpCode = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_CHANGE_OTP,
|
||||
userId,
|
||||
// Use aliasId to store the new email (we'll parse this back later)
|
||||
aliasId: newEmail.toLowerCase()
|
||||
payload: newEmail.toLowerCase()
|
||||
});
|
||||
|
||||
// Send OTP to NEW email address
|
||||
@@ -271,18 +272,16 @@ export const userServiceFactory = ({
|
||||
code: otpCode
|
||||
});
|
||||
} catch (error) {
|
||||
// For security reasons, always return "Invalid verification code" regardless of the actual error
|
||||
// This prevents information disclosure about existing emails
|
||||
throw new BadRequestError({ message: "Invalid verification code", name: "UpdateUserEmail" });
|
||||
}
|
||||
|
||||
// Verify the new email matches what was stored in aliasId
|
||||
const tokenNewEmail = tokenData?.aliasId;
|
||||
// Verify the new email matches what was stored in payload
|
||||
const tokenNewEmail = tokenData?.payload;
|
||||
if (!tokenNewEmail || tokenNewEmail !== newEmail.toLowerCase()) {
|
||||
throw new BadRequestError({ message: "Invalid verification code", name: "UpdateUserEmail" });
|
||||
}
|
||||
|
||||
// Final check if another user has this email (in case it was taken between OTP request and verification)
|
||||
// Final check if another user has this email
|
||||
const existingUsers = await userDAL.findUserByUsername(newEmail.toLowerCase(), tx);
|
||||
const existingUser = existingUsers?.find((u) => u.id !== userId);
|
||||
if (existingUser) {
|
||||
@@ -292,13 +291,11 @@ export const userServiceFactory = ({
|
||||
// Delete all user aliases since the email is changing
|
||||
await userAliasDAL.delete({ userId }, tx);
|
||||
|
||||
// Update the user's email and KEEP email as verified (as requested)
|
||||
const updatedUser = await userDAL.updateById(
|
||||
userId,
|
||||
{
|
||||
email: newEmail.toLowerCase(),
|
||||
username: newEmail.toLowerCase(),
|
||||
isEmailVerified: true // Keep verified as per requirement
|
||||
username: newEmail.toLowerCase()
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -30,8 +30,8 @@ You can update your account email address:
|
||||
2. Navigate to the `Authentication` tab.
|
||||
3. In the `Change Email` section, enter your new email address.
|
||||

|
||||
4. Click `Send Verification Code` to receive an 8-digit verification code at your new email address.
|
||||
5. Check your new email inbox and enter the verification code in the form.
|
||||
4. Click `Send Verification Code` to receive an 6-digit verification code at your new email address.
|
||||
5. Check your new email inbox and enter the verification code.
|
||||

|
||||
6. Click `Confirm Email Change` to complete the process.
|
||||
7. You will be logged out and need to sign in again with your new email address.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 766 KiB After Width: | Height: | Size: 437 KiB |
@@ -156,7 +156,7 @@ export const useRemoveMyDuplicateAccounts = () => {
|
||||
export const useRequestEmailChangeOTP = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ newEmail }: { newEmail: string }) => {
|
||||
const { data } = await apiRequest.post("/api/v2/users/me/email/otp", {
|
||||
const { data } = await apiRequest.post("/api/v2/users/me/email-change/otp", {
|
||||
newEmail
|
||||
});
|
||||
return data;
|
||||
|
||||
@@ -103,9 +103,9 @@ export const ChangeEmailSection = () => {
|
||||
const [typedOTP, setTypedOTP] = useState("");
|
||||
|
||||
const handleOTPSubmit = async () => {
|
||||
if (typedOTP.length !== 8) {
|
||||
if (typedOTP.length !== 6) {
|
||||
createNotification({
|
||||
text: "Please enter the complete 8-digit verification code",
|
||||
text: "Please enter the complete 6-digit verification code",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
@@ -197,7 +197,7 @@ export const ChangeEmailSection = () => {
|
||||
Send Verification Code
|
||||
</Button>
|
||||
<p className="mt-2 font-inter text-sm text-mineshaft-400">
|
||||
We'll send an 8-digit verification code to your new email address.
|
||||
We'll send an 6-digit verification code to your new email address.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@ export const ChangeEmailSection = () => {
|
||||
>
|
||||
<ModalContent
|
||||
title="Email Verification"
|
||||
subTitle={`Enter the 8-digit verification code sent to: ${pendingEmail}`}
|
||||
subTitle={`Enter the 6-digit verification code sent to: ${pendingEmail}`}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
@@ -218,7 +218,7 @@ export const ChangeEmailSection = () => {
|
||||
name="otp-input"
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={8}
|
||||
fields={6}
|
||||
onChange={setTypedOTP}
|
||||
value={typedOTP}
|
||||
{...otpInputProps}
|
||||
@@ -232,7 +232,7 @@ export const ChangeEmailSection = () => {
|
||||
<Button
|
||||
onClick={handleOTPSubmit}
|
||||
isLoading={isUpdatingEmail}
|
||||
isDisabled={typedOTP.length !== 8}
|
||||
isDisabled={typedOTP.length !== 6}
|
||||
>
|
||||
Confirm Email Change
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user