mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 16:27:40 +00:00
Fix SAML duplicate accounts when signing in the first time on an existing account
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified"))) {
|
||||||
|
// Add the column
|
||||||
|
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||||
|
t.boolean("isEmailVerified").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliasesToUpdate: { aliasId: string; isEmailVerified: boolean }[] = await knex(TableName.UserAliases)
|
||||||
|
.join(TableName.Users, `${TableName.UserAliases}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select([`${TableName.UserAliases}.id as aliasId`, `${TableName.Users}.isEmailVerified`]);
|
||||||
|
|
||||||
|
for (let i = 0; i < aliasesToUpdate.length; i += BATCH_SIZE) {
|
||||||
|
const batch = aliasesToUpdate.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
|
const trueIds = batch.filter((row) => row.isEmailVerified).map((row) => row.aliasId);
|
||||||
|
|
||||||
|
if (trueIds.length > 0) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.UserAliases).whereIn("id", trueIds).update({ isEmailVerified: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified")) {
|
||||||
|
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||||
|
t.dropColumn("isEmailVerified");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ export const UserAliasesSchema = z.object({
|
|||||||
emails: z.string().array().nullable().optional(),
|
emails: z.string().array().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAlias = await userAliasDAL.findOne({
|
let userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.SAML
|
aliasType: UserAliasType.SAML
|
||||||
@@ -320,15 +320,13 @@ export const samlConfigServiceFactory = ({
|
|||||||
|
|
||||||
user = await userDAL.transaction(async (tx) => {
|
user = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
if (serverCfg.trustSamlEmails) {
|
newUser = await userDAL.findOne(
|
||||||
newUser = await userDAL.findOne(
|
{
|
||||||
{
|
email,
|
||||||
email,
|
isEmailVerified: true
|
||||||
isEmailVerified: true
|
},
|
||||||
},
|
tx
|
||||||
tx
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
||||||
@@ -346,13 +344,14 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await userAliasDAL.create(
|
userAlias = await userAliasDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
aliasType: UserAliasType.SAML,
|
aliasType: UserAliasType.SAML,
|
||||||
externalId,
|
externalId,
|
||||||
emails: email ? [email] : [],
|
emails: email ? [email] : [],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustSamlEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -410,13 +409,13 @@ export const samlConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
|
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified && userAlias.isEmailVerified);
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
@@ -440,7 +439,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
|
|
||||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !userAlias.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
|
|||||||
@@ -727,7 +727,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectMembershipDAL
|
projectMembershipDAL,
|
||||||
|
userAliasDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const totpService = totpServiceFactory({
|
const totpService = totpServiceFactory({
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
headers: z.object({
|
||||||
|
referer: z.string().trim()
|
||||||
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
username: z.string().trim()
|
username: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -25,7 +28,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.user.sendEmailVerificationCode(req.body.username);
|
await server.services.user.sendEmailVerificationCode(req.body.username, req.headers.referer);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
@@ -12,6 +13,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ type TUserServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
|
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "find" | "update">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
@@ -48,17 +51,28 @@ export const userServiceFactory = ({
|
|||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
userAliasDAL
|
||||||
}: TUserServiceFactoryDep) => {
|
}: TUserServiceFactoryDep) => {
|
||||||
const sendEmailVerificationCode = async (username: string) => {
|
const sendEmailVerificationCode = async (username: string, referer: string) => {
|
||||||
// akhilmhdh: case sensitive email resolution
|
// akhilmhdh: case sensitive email resolution
|
||||||
const users = await userDAL.findUserByUsername(username);
|
const users = await userDAL.findUserByUsername(username);
|
||||||
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
|
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
|
||||||
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
||||||
|
let { isEmailVerified } = user;
|
||||||
|
const url = new URL(referer);
|
||||||
|
const refererToken = url.searchParams.get("token");
|
||||||
|
if (!refererToken)
|
||||||
|
throw new BadRequestError({ name: "Failed to send email verification code due to no token on referer" });
|
||||||
|
const { authType } = crypto.jwt().decode(refererToken) as { authType: string };
|
||||||
|
const userAlias = await userAliasDAL.findOne({ userId: user.id, aliasType: authType });
|
||||||
|
if (userAlias) {
|
||||||
|
isEmailVerified = userAlias.isEmailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.email)
|
if (!user.email)
|
||||||
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
||||||
if (user.isEmailVerified)
|
if (isEmailVerified)
|
||||||
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
|
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
|
||||||
|
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
@@ -95,7 +109,9 @@ export const userServiceFactory = ({
|
|||||||
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
||||||
if (!user.email)
|
if (!user.email)
|
||||||
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
|
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
|
||||||
if (user.isEmailVerified)
|
|
||||||
|
const userAliases = await userAliasDAL.find({ userId: user.id });
|
||||||
|
if (user.isEmailVerified && userAliases?.every((alias) => alias.isEmailVerified))
|
||||||
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
||||||
|
|
||||||
await tokenService.validateTokenForUser({
|
await tokenService.validateTokenForUser({
|
||||||
@@ -106,6 +122,8 @@ export const userServiceFactory = ({
|
|||||||
|
|
||||||
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
||||||
|
|
||||||
|
await userAliasDAL.update({ userId: user.id }, { isEmailVerified: true });
|
||||||
|
|
||||||
await userDAL.updateById(user.id, {
|
await userDAL.updateById(user.id, {
|
||||||
isEmailVerified: true,
|
isEmailVerified: true,
|
||||||
username: userEmails?.length === 1 && userEmails?.[0]?.id === user.id ? user.email.toLowerCase() : undefined
|
username: userEmails?.length === 1 && userEmails?.[0]?.id === user.id ? user.email.toLowerCase() : undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user