fix: select sub-org handler

This commit is contained in:
Piyush Gupta
2025-11-27 21:26:15 +05:30
parent 2a86b73c5f
commit 5464729c04
5 changed files with 113 additions and 198 deletions

View File

@@ -2693,7 +2693,7 @@ interface SelectSubOrganizationEvent {
metadata: {
organizationId: string;
organizationName: string;
parentOrganizationId: string;
rootOrganizationId: string;
};
}

View File

@@ -43,10 +43,15 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit
},
schema: {
body: z.object({
organizationId: z.string().trim(),
userAgent: z.enum(["cli"]).optional()
}),
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"
}),
response: {
200: z.object({
token: z.string(),
@@ -57,12 +62,25 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
},
handler: async (req, res) => {
const cfg = getConfig();
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
});
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
});
}
if (tokens.isMfaEnabled) {
return {
@@ -75,7 +93,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
if (githubOauthAccessToken) {
await server.services.githubOrgSync
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
.syncUserGroups(targetOrgId, tokens.user.userId, githubOauthAccessToken)
.finally(() => {
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
httpOnly: true,
@@ -108,74 +126,6 @@ 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({
method: "POST",
url: "/login2",

View File

@@ -725,39 +725,58 @@ export const authLoginServiceFactory = ({
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" });
// 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 (!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) {
if (!userSubOrgMembership) {
throw new ForbiddenRequestError({
message: "Sub-organization does not belong to the current organization"
message: `User does not have access to the sub-organization named ${subOrg.name}`
});
}
// Check user membership in the sub-organization
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: subOrganizationId,
scope: AccessScope.Organization
});
const subOrgmembershipRole = await membershipRoleDAL.findOne({ membershipId: userSubOrgMembership.id });
if (!orgMembership) {
throw new ForbiddenRequestError({ message: "User is not a member of this sub-organization" });
// 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 (!orgMembership.isActive) {
throw new ForbiddenRequestError({ message: "User membership in sub-organization is inactive" });
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
@@ -797,8 +816,8 @@ export const authLoginServiceFactory = ({
user,
userAgent,
ip: ipAddress,
organizationId: decodedToken.organizationId, // Keep root org ID
subOrganizationId, // Add sub-org ID
...(subOrg.rootOrgId && { organizationId: subOrg.rootOrgId }),
subOrganizationId,
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
});
@@ -823,7 +842,7 @@ export const authLoginServiceFactory = ({
metadata: {
organizationId: subOrganizationId,
organizationName: subOrg.name,
parentOrganizationId: decodedToken.organizationId
rootOrganizationId: subOrg.rootOrgId ?? ""
}
}
});
@@ -831,12 +850,7 @@ export const authLoginServiceFactory = ({
return {
...tokens,
user,
isMfaEnabled: false,
subOrganization: {
id: subOrg.id,
name: subOrg.name,
slug: subOrg.slug
}
isMfaEnabled: false
};
};

View File

@@ -58,10 +58,17 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return data;
};
export const selectOrganization = async (data: {
organizationId: string;
userAgent?: UserAgentType;
}) => {
export type SelectOrganizationParams =
| {
organizationId: string;
userAgent?: UserAgentType;
}
| {
subOrganizationId: string;
userAgent?: UserAgentType;
};
export const selectOrganization = async (data: SelectOrganizationParams) => {
const { data: res } = await apiRequest.post<{
token: string;
isMfaEnabled: boolean;
@@ -73,7 +80,7 @@ export const selectOrganization = async (data: {
export const useSelectOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
mutationFn: async (details: SelectOrganizationParams) => {
const data = await selectOrganization(details);
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
@@ -107,49 +114,6 @@ 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 = () => {
return useMutation({
mutationFn: async (details: {

View File

@@ -62,7 +62,11 @@ import {
useGetOrgTrialUrl,
useLogoutUser
} from "@app/hooks/api";
import { authKeys, selectOrganization, selectSubOrganization } from "@app/hooks/api/auth/queries";
import {
authKeys,
selectOrganization,
type SelectOrganizationParams
} from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
@@ -191,12 +195,26 @@ export const Navbar = () => {
}
}, [subscription, isBillingPage, isModalIntrusive]);
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => {
if (orgId === currentOrg.id) return;
const handleOrgSelection = async ({
organizationId,
subOrganizationId,
onSuccess
}: {
organizationId?: string;
subOrganizationId?: string;
onSuccess?: () => void | Promise<void>;
}) => {
if (!organizationId && !subOrganizationId) return;
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
const targetId = subOrganizationId ?? organizationId;
if (targetId === currentOrg.id) return;
const selectionPayload: SelectOrganizationParams = subOrganizationId
? { subOrganizationId }
: { organizationId: organizationId as string };
const { token, isMfaEnabled, mfaMethod } = await selectOrganization(selectionPayload);
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
@@ -205,7 +223,7 @@ export const Navbar = () => {
}
toggleShowMfa.on();
setMfaSuccessCallback(() => async () => {
await handleOrgChange(orgId, onSuccess);
await handleOrgSelection({ organizationId, subOrganizationId, onSuccess });
});
return;
}
@@ -216,7 +234,7 @@ export const Navbar = () => {
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
await navigateUserToOrg(navigate, targetId);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
if (onSuccess) {
@@ -234,43 +252,12 @@ export const Navbar = () => {
};
if (currentOrg.id !== rootOrg.id) {
await handleOrgChange(rootOrg.id, navigateToBilling);
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess: navigateToBilling });
} else {
await navigateToBilling();
}
};
const handleSubOrgChange = async (subOrgId: string) => {
if (subOrgId === currentOrg.id) return;
const { token, isMfaEnabled, mfaMethod } = await selectSubOrganization({
subOrganizationId: subOrgId
});
localStorage.setItem("orgData.id", subOrgId);
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleSubOrgChange(subOrgId));
return;
}
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate();
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrgId }
});
};
const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser();
@@ -340,7 +327,7 @@ export const Navbar = () => {
return;
}
handleOrgChange(org?.id);
handleOrgSelection({ organizationId: org?.id });
};
return (
@@ -465,7 +452,7 @@ export const Navbar = () => {
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={() => handleSubOrgChange(subOrg.id)}
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
className="cursor-pointer font-normal"
key={subOrg.id}
>
@@ -563,7 +550,7 @@ export const Navbar = () => {
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={() => handleSubOrgChange(subOrg.id)}
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
className="cursor-pointer font-normal"
key={subOrg.id}
>