Improve verification and resend code logic, added oidc and ldap

This commit is contained in:
Carlos Monastyrski
2025-08-12 18:58:23 -07:00
parent 60b3f5c7c6
commit 8a72023e80
11 changed files with 101 additions and 74 deletions
@@ -26,6 +26,12 @@ export async function up(knex: Knex): Promise<void> {
} }
} }
} }
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> { export async function down(knex: Knex): Promise<void> {
@@ -34,4 +40,10 @@ export async function down(knex: Knex): Promise<void> {
t.dropColumn("isEmailVerified"); t.dropColumn("isEmailVerified");
}); });
} }
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.dropColumn("aliasId");
});
}
} }
+2 -1
View File
@@ -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>;
@@ -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
@@ -443,7 +443,8 @@ export const samlConfigServiceFactory = ({
if (user.email && !userAlias.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({
+3 -3
View File
@@ -17,15 +17,15 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}) })
}, },
schema: { schema: {
headers: z.object({ body: z.object({
referer: 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.headers.referer); 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 = {
+23 -21
View File
@@ -10,7 +10,7 @@ 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 { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
@@ -39,7 +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">; userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "find" | "updateById">;
}; };
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>; export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
@@ -54,23 +54,23 @@ export const userServiceFactory = ({
permissionService, permissionService,
userAliasDAL userAliasDAL
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (referer: string) => { const sendEmailVerificationCode = async (token: string) => {
const url = new URL(referer); const { authType, aliasId, username, authTokenType } = crypto.jwt().decode(token) as {
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, aliasId, username } = crypto.jwt().decode(refererToken) as {
authType: string; authType: string;
aliasId: string; aliasId?: string;
username: 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; let { isEmailVerified } = user;
const userAlias = await userAliasDAL.findOne({ userId: user.id, aliasType: authType, id: aliasId }); if (aliasId) {
if (userAlias) { 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; isEmailVerified = userAlias.isEmailVerified;
} }
@@ -79,9 +79,10 @@ export const userServiceFactory = ({
if (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({
@@ -89,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
} }
}); });
}; };
@@ -114,19 +115,20 @@ export const userServiceFactory = ({
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" });
const userAliases = await userAliasDAL.find({ userId: user.id }); const token = await tokenService.validateTokenForUser({
if (user.isEmailVerified && userAliases?.every((alias) => alias.isEmailVerified))
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION, type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id, userId: user.id,
code code
}); });
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : []; if (token?.aliasId) {
const userAlias = await userAliasDAL.findOne({ userId: user.id, id: token.aliasId });
if (userAlias?.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await userAliasDAL.update({ userId: user.id }, { isEmailVerified: true }); await userAliasDAL.updateById(token.aliasId, { isEmailVerified: true });
}
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
await userDAL.updateById(user.id, { await userDAL.updateById(user.id, {
isEmailVerified: true, isEmailVerified: true,
+4 -4
View File
@@ -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 {};
} }
}); });
@@ -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"