mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 20:26:17 +00:00
Merge pull request #4344 from Infisical/fix/samlDuplicateAccounts
Fix SAML duplicate accounts when signing in the first time on an existing account
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.AuthTokens, "aliasId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||||
|
t.string("aliasId").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
|
||||||
|
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||||
|
t.dropColumn("aliasId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ export const AuthTokensSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional()
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
|
aliasId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -400,15 +400,13 @@ export const ldapConfigServiceFactory = ({
|
|||||||
|
|
||||||
userAlias = await userDAL.transaction(async (tx) => {
|
userAlias = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
if (serverCfg.trustLdapEmails) {
|
newUser = await userDAL.findOne(
|
||||||
newUser = await userDAL.findOne(
|
{
|
||||||
{
|
email: email.toLowerCase(),
|
||||||
email: email.toLowerCase(),
|
isEmailVerified: true
|
||||||
isEmailVerified: true
|
},
|
||||||
},
|
tx
|
||||||
tx
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||||
@@ -433,7 +431,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
aliasType: UserAliasType.LDAP,
|
aliasType: UserAliasType.LDAP,
|
||||||
externalId,
|
externalId,
|
||||||
emails: [email],
|
emails: [email],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustLdapEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -556,15 +555,14 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted) && 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,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
...(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,
|
||||||
@@ -572,6 +570,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: AuthMethod.LDAP,
|
authMethod: AuthMethod.LDAP,
|
||||||
authType: UserAliasType.LDAP,
|
authType: UserAliasType.LDAP,
|
||||||
|
aliasId: userAlias.id,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
? {
|
? {
|
||||||
@@ -585,10 +584,11 @@ export const ldapConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const userAlias = await userAliasDAL.findOne({
|
let userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.OIDC
|
aliasType: UserAliasType.OIDC
|
||||||
@@ -231,32 +231,29 @@ export const oidcConfigServiceFactory = ({
|
|||||||
} else {
|
} else {
|
||||||
user = await userDAL.transaction(async (tx) => {
|
user = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
|
// we prioritize getting the most complete user to create the new alias under
|
||||||
|
newUser = await userDAL.findOne(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
isEmailVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
if (serverCfg.trustOidcEmails) {
|
if (!newUser) {
|
||||||
// we prioritize getting the most complete user to create the new alias under
|
// this fetches user entries created via invites
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.findOne(
|
||||||
{
|
{
|
||||||
email,
|
username: email
|
||||||
isEmailVerified: true
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newUser) {
|
if (newUser && !newUser.isEmailVerified) {
|
||||||
// this fetches user entries created via invites
|
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.updateById(newUser.id, {
|
||||||
{
|
isEmailVerified: serverCfg.trustOidcEmails
|
||||||
username: email
|
});
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newUser && !newUser.isEmailVerified) {
|
|
||||||
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
|
||||||
newUser = await userDAL.updateById(newUser.id, {
|
|
||||||
isEmailVerified: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +273,14 @@ export const oidcConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await userAliasDAL.create(
|
userAlias = await userAliasDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
aliasType: UserAliasType.OIDC,
|
aliasType: UserAliasType.OIDC,
|
||||||
externalId,
|
externalId,
|
||||||
emails: email ? [email] : [],
|
emails: email ? [email] : [],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustOidcEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -404,19 +402,20 @@ export const oidcConfigServiceFactory = ({
|
|||||||
|
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted) && 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,
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
|
aliasId: userAlias.id,
|
||||||
authMethod: AuthMethod.OIDC,
|
authMethod: AuthMethod.OIDC,
|
||||||
authType: UserAliasType.OIDC,
|
authType: UserAliasType.OIDC,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
@@ -430,10 +429,11 @@ export const oidcConfigServiceFactory = ({
|
|||||||
|
|
||||||
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
await oidcConfigDAL.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,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService
|
await smtpService
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -424,6 +423,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: authProvider,
|
authMethod: authProvider,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
|
aliasId: userAlias.id,
|
||||||
authType: UserAliasType.SAML,
|
authType: UserAliasType.SAML,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
@@ -440,10 +440,11 @@ 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,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
|
|||||||
@@ -726,7 +726,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectMembershipDAL
|
projectMembershipDAL,
|
||||||
|
userAliasDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const totpService = totpServiceFactory({
|
const totpService = totpServiceFactory({
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
username: z.string().trim()
|
token: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.object({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.user.sendEmailVerificationCode(req.body.username);
|
await server.services.user.sendEmailVerificationCode(req.body.token);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
||||||
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => {
|
||||||
const { token, ...tkCfg } = getTokenConfig(type);
|
const { token, ...tkCfg } = getTokenConfig(type);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
||||||
@@ -88,7 +88,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
type,
|
type,
|
||||||
userId,
|
userId,
|
||||||
orgId,
|
orgId,
|
||||||
triesLeft: tkCfg?.triesLeft
|
triesLeft: tkCfg?.triesLeft,
|
||||||
|
aliasId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type TCreateTokenForUserDTO = {
|
|||||||
type: TokenType;
|
type: TokenType;
|
||||||
userId: string;
|
userId: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
aliasId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateOrgInviteTokenDTO = {
|
export type TCreateOrgInviteTokenDTO = {
|
||||||
|
|||||||
@@ -453,6 +453,13 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
const selectedOrg = await orgDAL.findById(organizationId);
|
const selectedOrg = await orgDAL.findById(organizationId);
|
||||||
|
|
||||||
|
// Check if authEnforced is true, if that's the case, throw an error
|
||||||
|
if (selectedOrg.authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Authentication is required by your organization before you can log in."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedOrgMembership) {
|
if (!selectedOrgMembership) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -9,9 +10,10 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
|
|||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod, AuthTokenType } 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" | "updateById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
@@ -48,22 +51,38 @@ export const userServiceFactory = ({
|
|||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
userAliasDAL
|
||||||
}: TUserServiceFactoryDep) => {
|
}: TUserServiceFactoryDep) => {
|
||||||
const sendEmailVerificationCode = async (username: string) => {
|
const sendEmailVerificationCode = async (token: string) => {
|
||||||
|
const { authType, aliasId, username, authTokenType } = crypto.jwt().decode(token) as {
|
||||||
|
authType: string;
|
||||||
|
aliasId?: string;
|
||||||
|
username: string;
|
||||||
|
authTokenType: AuthTokenType;
|
||||||
|
};
|
||||||
|
if (authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new BadRequestError({ name: "Invalid auth token type" });
|
||||||
|
|
||||||
// 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;
|
||||||
|
if (aliasId) {
|
||||||
|
const userAlias = await userAliasDAL.findOne({ userId: user.id, aliasType: authType, id: aliasId });
|
||||||
|
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${aliasId}' not found` });
|
||||||
|
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 userToken = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
aliasId
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
@@ -71,7 +90,7 @@ export const userServiceFactory = ({
|
|||||||
subjectLine: "Infisical confirmation code",
|
subjectLine: "Infisical confirmation code",
|
||||||
recipients: [user.email],
|
recipients: [user.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
code: userToken
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -95,15 +114,21 @@ 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)
|
|
||||||
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
|
||||||
|
|
||||||
await tokenService.validateTokenForUser({
|
const token = await tokenService.validateTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (token?.aliasId) {
|
||||||
|
const userAlias = await userAliasDAL.findOne({ userId: user.id, id: token.aliasId });
|
||||||
|
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${token.aliasId}' not found` });
|
||||||
|
if (userAlias?.isEmailVerified)
|
||||||
|
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
||||||
|
|
||||||
|
await userAliasDAL.updateById(token.aliasId, { isEmailVerified: true });
|
||||||
|
}
|
||||||
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
||||||
|
|
||||||
await userDAL.updateById(user.id, {
|
await userDAL.updateById(user.id, {
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ export const useAddUserToWsNonE2EE = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendEmailVerificationCode = async (username: string) => {
|
export const sendEmailVerificationCode = async (token: string) => {
|
||||||
return apiRequest.post("/api/v2/users/me/emails/code", {
|
return apiRequest.post("/api/v2/users/me/emails/code", {
|
||||||
username
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSendEmailVerificationCode = () => {
|
export const useSendEmailVerificationCode = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (username: string) => {
|
mutationFn: async (token: string) => {
|
||||||
await sendEmailVerificationCode(username);
|
await sendEmailVerificationCode(token);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-1
@@ -114,7 +114,16 @@ export const EmailConfirmationStep = ({
|
|||||||
|
|
||||||
const resendCode = async () => {
|
const resendCode = async () => {
|
||||||
try {
|
try {
|
||||||
await sendEmailVerificationCode(username);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
const token = queryParams.get("token");
|
||||||
|
if (!token) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to resend code, no token found",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendEmailVerificationCode(token);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully resent code",
|
text: "Successfully resent code",
|
||||||
type: "success"
|
type: "success"
|
||||||
|
|||||||
Reference in New Issue
Block a user