mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
fix: suborg routing
This commit is contained in:
@@ -108,6 +108,74 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/select-sub-organization",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
subOrganizationId: z.string().trim(),
|
||||||
|
userAgent: z.enum(["cli"]).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
token: z.string(),
|
||||||
|
isMfaEnabled: z.boolean(),
|
||||||
|
mfaMethod: z.string().optional(),
|
||||||
|
subOrganization: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
const result = await server.services.login.selectSubOrganization({
|
||||||
|
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||||
|
authJwtToken: req.headers.authorization,
|
||||||
|
subOrganizationId: req.body.subOrganizationId,
|
||||||
|
ipAddress: req.realIp
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isMfaEnabled) {
|
||||||
|
return {
|
||||||
|
token: result.mfa as string,
|
||||||
|
isMfaEnabled: true,
|
||||||
|
mfaMethod: result.mfaMethod
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void res.setCookie("jid", result.refresh, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
addAuthOriginDomainCookie(res);
|
||||||
|
|
||||||
|
void res.cookie("infisical-project-assume-privileges", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: result.access,
|
||||||
|
isMfaEnabled: false,
|
||||||
|
subOrganization: result.subOrganization
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/login2",
|
url: "/login2",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export const authLoginServiceFactory = ({
|
|||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
organizationId,
|
organizationId,
|
||||||
|
subOrganizationId,
|
||||||
authMethod,
|
authMethod,
|
||||||
isMfaVerified,
|
isMfaVerified,
|
||||||
mfaMethod
|
mfaMethod
|
||||||
@@ -150,6 +151,7 @@ export const authLoginServiceFactory = ({
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
|
subOrganizationId?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
mfaMethod?: MfaMethod;
|
mfaMethod?: MfaMethod;
|
||||||
@@ -193,6 +195,7 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
accessVersion: tokenSession.accessVersion,
|
accessVersion: tokenSession.accessVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
|
subOrganizationId,
|
||||||
isMfaVerified,
|
isMfaVerified,
|
||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
@@ -208,6 +211,7 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
refreshVersion: tokenSession.refreshVersion,
|
refreshVersion: tokenSession.refreshVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
|
subOrganizationId,
|
||||||
isMfaVerified,
|
isMfaVerified,
|
||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
@@ -701,6 +705,141 @@ export const authLoginServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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" });
|
||||||
|
if (!decodedToken.organizationId)
|
||||||
|
throw new BadRequestError({ message: "No organization selected in current token" });
|
||||||
|
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
||||||
|
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
||||||
|
|
||||||
|
// Fetch the sub-organization
|
||||||
|
const subOrg = await orgDAL.findById(subOrganizationId);
|
||||||
|
if (!subOrg) {
|
||||||
|
throw new BadRequestError({ message: `Sub-organization with ID ${subOrganizationId} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this is actually a sub-organization of the current root org
|
||||||
|
if (subOrg.rootOrgId !== decodedToken.organizationId && subOrg.id !== decodedToken.organizationId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Sub-organization does not belong to the current organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user membership in the sub-organization
|
||||||
|
const orgMembership = await membershipUserDAL.findOne({
|
||||||
|
actorUserId: user.id,
|
||||||
|
scopeOrgId: subOrganizationId,
|
||||||
|
scope: AccessScope.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgMembership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "User is not a member of this sub-organization" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgMembership.isActive) {
|
||||||
|
throw new ForbiddenRequestError({ message: "User membership in sub-organization is inactive" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
cfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: cfg.JWT_MFA_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mfaMethod === MfaMethod.EMAIL && user.email) {
|
||||||
|
await sendUserMfaCode({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tokens scoped to the sub-organization
|
||||||
|
const tokens = await generateUserTokens({
|
||||||
|
authMethod: decodedToken.authMethod,
|
||||||
|
user,
|
||||||
|
userAgent,
|
||||||
|
ip: ipAddress,
|
||||||
|
organizationId: decodedToken.organizationId, // Keep root org ID
|
||||||
|
subOrganizationId, // Add sub-org ID
|
||||||
|
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,
|
||||||
|
parentOrganizationId: decodedToken.organizationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...tokens,
|
||||||
|
user,
|
||||||
|
isMfaEnabled: false,
|
||||||
|
subOrganization: {
|
||||||
|
id: subOrg.id,
|
||||||
|
name: subOrg.name,
|
||||||
|
slug: subOrg.slug
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Multi factor authentication re-send code, Get user id from token
|
* Multi factor authentication re-send code, Get user id from token
|
||||||
* saved in frontend
|
* saved in frontend
|
||||||
@@ -1134,6 +1273,7 @@ export const authLoginServiceFactory = ({
|
|||||||
resendMfaToken,
|
resendMfaToken,
|
||||||
verifyMfaToken,
|
verifyMfaToken,
|
||||||
selectOrganization,
|
selectOrganization,
|
||||||
|
selectSubOrganization,
|
||||||
generateUserTokens,
|
generateUserTokens,
|
||||||
login
|
login
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,6 +107,49 @@ export const useSelectOrganization = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectSubOrganization = async (data: {
|
||||||
|
subOrganizationId: string;
|
||||||
|
userAgent?: UserAgentType;
|
||||||
|
}) => {
|
||||||
|
const { data: res } = await apiRequest.post<{
|
||||||
|
token: string;
|
||||||
|
isMfaEnabled: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
|
subOrganization?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}>("/api/v3/auth/select-sub-organization", data);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSelectSubOrganization = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (details: { subOrganizationId: string; userAgent?: UserAgentType }) => {
|
||||||
|
const data = await selectSubOrganization(details);
|
||||||
|
|
||||||
|
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
|
||||||
|
if (!details.userAgent && !data.isMfaEnabled) {
|
||||||
|
SecurityClient.setToken(data.token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.token && !data.isMfaEnabled) {
|
||||||
|
setAuthToken(data.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [organizationKeys.getUserOrganizations, projectKeys.getAllUserProjects]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useLogin2 = () => {
|
export const useLogin2 = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (details: {
|
mutationFn: async (details: {
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ export const Navbar = () => {
|
|||||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||||
|
|
||||||
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => {
|
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => {
|
||||||
|
if (orgId === currentOrg.id) return;
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
organizationId: orgId
|
organizationId: orgId
|
||||||
});
|
});
|
||||||
@@ -243,6 +245,8 @@ export const Navbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubOrgChange = async (subOrgId: string) => {
|
const handleSubOrgChange = async (subOrgId: string) => {
|
||||||
|
if (subOrgId === currentOrg.id) return;
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod } = await selectSubOrganization({
|
const { token, isMfaEnabled, mfaMethod } = await selectSubOrganization({
|
||||||
subOrganizationId: subOrgId
|
subOrganizationId: subOrgId
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user