mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
fix: review changes
This commit is contained in:
@@ -43,15 +43,10 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
organizationId: z.string().trim().optional(),
|
||||
subOrganizationId: z.string().trim().optional(),
|
||||
userAgent: z.enum(["cli"]).optional()
|
||||
})
|
||||
.refine((body) => Boolean(body.organizationId || body.subOrganizationId), {
|
||||
message: "organizationId or subOrganizationId is required"
|
||||
}),
|
||||
body: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
userAgent: z.enum(["cli"]).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
token: z.string(),
|
||||
@@ -62,25 +57,13 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const cfg = getConfig();
|
||||
let tokens;
|
||||
|
||||
const targetOrgId = req.body.subOrganizationId ?? req.body.organizationId ?? "";
|
||||
|
||||
if (req.body.subOrganizationId) {
|
||||
tokens = await server.services.login.selectSubOrganization({
|
||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||
authJwtToken: req.headers.authorization,
|
||||
subOrganizationId: req.body.subOrganizationId,
|
||||
ipAddress: req.realIp
|
||||
});
|
||||
} else {
|
||||
tokens = await server.services.login.selectOrganization({
|
||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||
authJwtToken: req.headers.authorization,
|
||||
organizationId: req.body.organizationId as string,
|
||||
ipAddress: req.realIp
|
||||
});
|
||||
}
|
||||
const tokens = await server.services.login.selectOrganization({
|
||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||
authJwtToken: req.headers.authorization,
|
||||
organizationId: req.body.organizationId,
|
||||
ipAddress: req.realIp
|
||||
});
|
||||
|
||||
if (tokens.isMfaEnabled) {
|
||||
return {
|
||||
@@ -93,7 +76,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
|
||||
if (githubOauthAccessToken) {
|
||||
await server.services.githubOrgSync
|
||||
.syncUserGroups(targetOrgId, tokens.user.userId, githubOauthAccessToken)
|
||||
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
|
||||
.finally(() => {
|
||||
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
|
||||
httpOnly: true,
|
||||
|
||||
@@ -13,7 +13,13 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
DatabaseError,
|
||||
ForbiddenRequestError,
|
||||
NotFoundError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||
@@ -530,25 +536,77 @@ export const authLoginServiceFactory = ({
|
||||
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
||||
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
|
||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrg = await orgDAL.findById(organizationId);
|
||||
if (!selectedOrg) {
|
||||
throw new NotFoundError({ message: `Organization with ID '${organizationId}' not found` });
|
||||
}
|
||||
|
||||
if (!selectedOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||
const isSubOrganization = Boolean(selectedOrg.rootOrgId && selectedOrg.id !== selectedOrg.rootOrgId);
|
||||
|
||||
let rootOrg = selectedOrg;
|
||||
let membershipRole;
|
||||
|
||||
if (isSubOrganization) {
|
||||
if (!selectedOrg.rootOrgId) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid sub-organization"
|
||||
});
|
||||
}
|
||||
|
||||
rootOrg = await orgDAL.findById(selectedOrg.rootOrgId);
|
||||
if (!rootOrg) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid root organization"
|
||||
});
|
||||
}
|
||||
|
||||
// Check user membership in the sub-organization
|
||||
const orgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: organizationId,
|
||||
scope: AccessScope.Organization,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (!orgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the sub-organization named ${selectedOrg.name}`
|
||||
});
|
||||
}
|
||||
|
||||
// Check user membership in the root organization
|
||||
const rootOrgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: rootOrg.id,
|
||||
scope: AccessScope.Organization,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (!rootOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User does not have access to the root organization"
|
||||
});
|
||||
}
|
||||
|
||||
membershipRole = (await membershipRoleDAL.findOne({ membershipId: orgMembership.id })).role;
|
||||
} else {
|
||||
// For root organizations, check membership using the existing method
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
if (!selectedOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the organization named ${selectedOrg.name}`
|
||||
});
|
||||
}
|
||||
membershipRole = selectedOrgMembership.userRole;
|
||||
}
|
||||
|
||||
// Check if authEnforced is true and the current auth method is not an enforced method
|
||||
if (
|
||||
selectedOrg.authEnforced &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||
!(selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin)
|
||||
!(selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with the auth method required by your organization."
|
||||
@@ -556,7 +614,7 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
|
||||
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin;
|
||||
|
||||
if (!canBypass) {
|
||||
throw new ForbiddenRequestError({
|
||||
@@ -607,7 +665,8 @@ export const authLoginServiceFactory = ({
|
||||
user,
|
||||
userAgent,
|
||||
ip: ipAddress,
|
||||
organizationId,
|
||||
organizationId: isSubOrganization ? rootOrg.id : organizationId,
|
||||
subOrganizationId: isSubOrganization ? organizationId : undefined,
|
||||
isMfaVerified: decodedToken.isMfaVerified,
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
});
|
||||
@@ -675,205 +734,55 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
authMethod: decodedToken.authMethod
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SELECT_ORGANIZATION,
|
||||
metadata: {
|
||||
organizationId,
|
||||
organizationName: selectedOrg.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
user,
|
||||
isMfaEnabled: false
|
||||
};
|
||||
};
|
||||
|
||||
const selectSubOrganization = async ({
|
||||
userAgent,
|
||||
authJwtToken,
|
||||
ipAddress,
|
||||
subOrganizationId
|
||||
}: {
|
||||
userAgent: string | undefined;
|
||||
authJwtToken: string | undefined;
|
||||
ipAddress: string;
|
||||
subOrganizationId: string;
|
||||
}) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
if (!authJwtToken) throw new UnauthorizedError({ name: "Authorization header is required" });
|
||||
if (!userAgent) throw new UnauthorizedError({ name: "User-Agent header is required" });
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
authJwtToken = authJwtToken.replace("Bearer ", "");
|
||||
|
||||
const decodedToken = crypto.jwt().verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
|
||||
|
||||
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
|
||||
|
||||
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
||||
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
||||
|
||||
// Check user membership in the sub-organization
|
||||
const userSubOrgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: subOrganizationId,
|
||||
scope: AccessScope.Organization,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
// Fetch the sub-organization
|
||||
const subOrg = await orgDAL.findById(subOrganizationId);
|
||||
|
||||
if (!userSubOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the sub-organization named ${subOrg.name}`
|
||||
});
|
||||
}
|
||||
|
||||
if (!subOrg.rootOrgId) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid sub-organization"
|
||||
});
|
||||
}
|
||||
|
||||
const rootOrg = await orgDAL.findById(subOrg.rootOrgId);
|
||||
|
||||
if (!rootOrg) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid root organization"
|
||||
});
|
||||
}
|
||||
|
||||
const rootOrgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: rootOrg.id,
|
||||
scope: AccessScope.Organization,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (!rootOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User does not have access to the root organization"
|
||||
});
|
||||
}
|
||||
|
||||
const subOrgmembershipRole = await membershipRoleDAL.findOne({ membershipId: userSubOrgMembership.id });
|
||||
|
||||
// Check if authEnforced is true and the current auth method is not an enforced method
|
||||
if (
|
||||
subOrg.authEnforced &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||
!(subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with the auth method required by your organization."
|
||||
});
|
||||
}
|
||||
|
||||
if (subOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||
const canBypass = subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin;
|
||||
|
||||
if (!canBypass) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
||||
error: "GoogleSsoEnforced"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
||||
await orgDAL.updateById(subOrg.id, {
|
||||
googleSsoAuthLastUsed: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Check MFA requirements for the sub-organization
|
||||
const shouldCheckMfa = subOrg.enforceMfa || user.isMfaEnabled;
|
||||
const orgMfaMethod = subOrg.enforceMfa ? (subOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
||||
|
||||
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
|
||||
const mfaToken = crypto.jwt().sign(
|
||||
{
|
||||
authMethod: decodedToken.authMethod,
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: user.id
|
||||
// Create audit log for organization selection
|
||||
if (isSubOrganization) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
authMethod: decodedToken.authMethod
|
||||
}
|
||||
},
|
||||
cfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: cfg.JWT_MFA_LIFETIME
|
||||
event: {
|
||||
type: EventType.SELECT_SUB_ORGANIZATION,
|
||||
metadata: {
|
||||
organizationId,
|
||||
organizationName: selectedOrg.name,
|
||||
rootOrganizationId: rootOrg.id
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (mfaMethod === MfaMethod.EMAIL && user.email) {
|
||||
await sendUserMfaCode({
|
||||
userId: user.id,
|
||||
email: user.email
|
||||
});
|
||||
}
|
||||
|
||||
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const;
|
||||
});
|
||||
} else {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
authMethod: decodedToken.authMethod
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SELECT_ORGANIZATION,
|
||||
metadata: {
|
||||
organizationId,
|
||||
organizationName: selectedOrg.name
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generate tokens scoped to the sub-organization
|
||||
const tokens = await generateUserTokens({
|
||||
authMethod: decodedToken.authMethod,
|
||||
user,
|
||||
userAgent,
|
||||
ip: ipAddress,
|
||||
organizationId: rootOrg.id,
|
||||
subOrganizationId,
|
||||
isMfaVerified: decodedToken.isMfaVerified,
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
});
|
||||
|
||||
// Create audit log for sub-organization selection
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: subOrganizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
authMethod: decodedToken.authMethod
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SELECT_SUB_ORGANIZATION,
|
||||
metadata: {
|
||||
organizationId: subOrganizationId,
|
||||
organizationName: subOrg.name,
|
||||
rootOrganizationId: subOrg.rootOrgId ?? ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
user,
|
||||
@@ -1314,7 +1223,6 @@ export const authLoginServiceFactory = ({
|
||||
resendMfaToken,
|
||||
verifyMfaToken,
|
||||
selectOrganization,
|
||||
selectSubOrganization,
|
||||
generateUserTokens,
|
||||
login
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user