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
@@ -2693,7 +2693,7 @@ interface SelectSubOrganizationEvent {
metadata: { metadata: {
organizationId: string; organizationId: string;
organizationName: string; organizationName: string;
parentOrganizationId: string; rootOrganizationId: string;
}; };
} }
+29 -79
View File
@@ -43,10 +43,15 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
schema: { schema: {
body: z.object({ body: z
organizationId: z.string().trim(), .object({
userAgent: z.enum(["cli"]).optional() 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: { response: {
200: z.object({ 200: z.object({
token: z.string(), token: z.string(),
@@ -57,12 +62,25 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req, res) => { handler: async (req, res) => {
const cfg = getConfig(); const cfg = getConfig();
const tokens = await server.services.login.selectOrganization({ let tokens;
userAgent: req.body.userAgent ?? req.headers["user-agent"],
authJwtToken: req.headers.authorization, const targetOrgId = req.body.subOrganizationId ?? req.body.organizationId ?? "";
organizationId: req.body.organizationId,
ipAddress: req.realIp 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) { if (tokens.isMfaEnabled) {
return { return {
@@ -75,7 +93,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN]; const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
if (githubOauthAccessToken) { if (githubOauthAccessToken) {
await server.services.githubOrgSync await server.services.githubOrgSync
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken) .syncUserGroups(targetOrgId, tokens.user.userId, githubOauthAccessToken)
.finally(() => { .finally(() => {
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", { void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
httpOnly: true, 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({ server.route({
method: "POST", method: "POST",
url: "/login2", url: "/login2",
+41 -27
View File
@@ -725,39 +725,58 @@ export const authLoginServiceFactory = ({
authJwtToken = authJwtToken.replace("Bearer ", ""); authJwtToken = authJwtToken.replace("Bearer ", "");
const decodedToken = crypto.jwt().verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; 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.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); const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" }); 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 // Fetch the sub-organization
const subOrg = await orgDAL.findById(subOrganizationId); 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 (!userSubOrgMembership) {
if (subOrg.rootOrgId !== decodedToken.organizationId && subOrg.id !== decodedToken.organizationId) {
throw new ForbiddenRequestError({ 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 subOrgmembershipRole = await membershipRoleDAL.findOne({ membershipId: userSubOrgMembership.id });
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: subOrganizationId,
scope: AccessScope.Organization
});
if (!orgMembership) { // Check if authEnforced is true and the current auth method is not an enforced method
throw new ForbiddenRequestError({ message: "User is not a member of this sub-organization" }); 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) { if (subOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
throw new ForbiddenRequestError({ message: "User membership in sub-organization is inactive" }); 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 // Check MFA requirements for the sub-organization
@@ -797,8 +816,8 @@ export const authLoginServiceFactory = ({
user, user,
userAgent, userAgent,
ip: ipAddress, ip: ipAddress,
organizationId: decodedToken.organizationId, // Keep root org ID ...(subOrg.rootOrgId && { organizationId: subOrg.rootOrgId }),
subOrganizationId, // Add sub-org ID subOrganizationId,
isMfaVerified: decodedToken.isMfaVerified, isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod mfaMethod: decodedToken.mfaMethod
}); });
@@ -823,7 +842,7 @@ export const authLoginServiceFactory = ({
metadata: { metadata: {
organizationId: subOrganizationId, organizationId: subOrganizationId,
organizationName: subOrg.name, organizationName: subOrg.name,
parentOrganizationId: decodedToken.organizationId rootOrganizationId: subOrg.rootOrgId ?? ""
} }
} }
}); });
@@ -831,12 +850,7 @@ export const authLoginServiceFactory = ({
return { return {
...tokens, ...tokens,
user, user,
isMfaEnabled: false, isMfaEnabled: false
subOrganization: {
id: subOrg.id,
name: subOrg.name,
slug: subOrg.slug
}
}; };
}; };
+12 -48
View File
@@ -58,10 +58,17 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return data; return data;
}; };
export const selectOrganization = async (data: { export type SelectOrganizationParams =
organizationId: string; | {
userAgent?: UserAgentType; organizationId: string;
}) => { userAgent?: UserAgentType;
}
| {
subOrganizationId: string;
userAgent?: UserAgentType;
};
export const selectOrganization = async (data: SelectOrganizationParams) => {
const { data: res } = await apiRequest.post<{ const { data: res } = await apiRequest.post<{
token: string; token: string;
isMfaEnabled: boolean; isMfaEnabled: boolean;
@@ -73,7 +80,7 @@ export const selectOrganization = async (data: {
export const useSelectOrganization = () => { export const useSelectOrganization = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { mutationFn: async (details: SelectOrganizationParams) => {
const data = await selectOrganization(details); 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. // 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 = () => { export const useLogin2 = () => {
return useMutation({ return useMutation({
mutationFn: async (details: { mutationFn: async (details: {
@@ -62,7 +62,11 @@ import {
useGetOrgTrialUrl, useGetOrgTrialUrl,
useLogoutUser useLogoutUser
} from "@app/hooks/api"; } 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 { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery"; import { getAuthToken } from "@app/hooks/api/reactQuery";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
@@ -191,12 +195,26 @@ export const Navbar = () => {
} }
}, [subscription, isBillingPage, isModalIntrusive]); }, [subscription, isBillingPage, isModalIntrusive]);
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => { const handleOrgSelection = async ({
if (orgId === currentOrg.id) return; organizationId,
subOrganizationId,
onSuccess
}: {
organizationId?: string;
subOrganizationId?: string;
onSuccess?: () => void | Promise<void>;
}) => {
if (!organizationId && !subOrganizationId) return;
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ const targetId = subOrganizationId ?? organizationId;
organizationId: orgId
}); if (targetId === currentOrg.id) return;
const selectionPayload: SelectOrganizationParams = subOrganizationId
? { subOrganizationId }
: { organizationId: organizationId as string };
const { token, isMfaEnabled, mfaMethod } = await selectOrganization(selectionPayload);
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
@@ -205,7 +223,7 @@ export const Navbar = () => {
} }
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => async () => { setMfaSuccessCallback(() => async () => {
await handleOrgChange(orgId, onSuccess); await handleOrgSelection({ organizationId, subOrganizationId, onSuccess });
}); });
return; return;
} }
@@ -216,7 +234,7 @@ export const Navbar = () => {
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate(); await router.invalidate();
await navigateUserToOrg(navigate, orgId); await navigateUserToOrg(navigate, targetId);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
if (onSuccess) { if (onSuccess) {
@@ -234,43 +252,12 @@ export const Navbar = () => {
}; };
if (currentOrg.id !== rootOrg.id) { if (currentOrg.id !== rootOrg.id) {
await handleOrgChange(rootOrg.id, navigateToBilling); await handleOrgSelection({ organizationId: rootOrg.id, onSuccess: navigateToBilling });
} else { } else {
await navigateToBilling(); 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 { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser(); const logout = useLogoutUser();
@@ -340,7 +327,7 @@ export const Navbar = () => {
return; return;
} }
handleOrgChange(org?.id); handleOrgSelection({ organizationId: org?.id });
}; };
return ( return (
@@ -465,7 +452,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleSubOrgChange(subOrg.id)} onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >
@@ -563,7 +550,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleSubOrgChange(subOrg.id)} onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >