From ea2707651c4f0e49d0eb6b9957a4c5d7dcc3382e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 14 Aug 2025 06:13:24 +0400 Subject: [PATCH 1/5] feat(sso): enforce google SSO on org-level --- backend/src/@types/fastify.d.ts | 1 + .../20250813214709_enforce-google-sso.ts | 38 ++++ backend/src/db/schemas/organizations.ts | 4 +- .../src/ee/services/license/license-types.ts | 1 + .../ee/services/permission/permission-dal.ts | 7 + .../ee/services/permission/permission-fns.ts | 9 +- .../services/permission/permission-service.ts | 2 + .../server/routes/v1/organization-router.ts | 1 + backend/src/server/routes/v1/sso-router.ts | 11 +- .../src/services/auth/auth-login-service.ts | 59 +++++- backend/src/services/auth/auth-login-type.ts | 1 + backend/src/services/org/org-schema.ts | 1 + backend/src/services/org/org-service.ts | 38 ++++ backend/src/services/org/org-types.ts | 1 + .../src/hooks/api/organization/queries.tsx | 2 + frontend/src/hooks/api/organization/types.ts | 2 + frontend/src/hooks/api/subscriptions/types.ts | 1 + .../components/NavBar/Navbar.tsx | 5 + .../auth/SelectOrgPage/SelectOrgSection.tsx | 30 ++- .../OrgSsoTab/OrgGeneralAuthSection.tsx | 179 +++++++++++++----- .../components/OrgSsoTab/OrgOIDCSection.tsx | 136 +------------ .../components/OrgSsoTab/OrgSSOSection.tsx | 11 +- .../components/OrgSsoTab/OrgSsoTab.tsx | 51 +++-- 23 files changed, 371 insertions(+), 220 deletions(-) create mode 100644 backend/src/db/migrations/20250813214709_enforce-google-sso.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index adf9489d4..f65f8a4fa 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -148,6 +148,7 @@ declare module "fastify" { interface Session { callbackPort: string; isAdminLogin: boolean; + orgSlug: string; } interface FastifyRequest { diff --git a/backend/src/db/migrations/20250813214709_enforce-google-sso.ts b/backend/src/db/migrations/20250813214709_enforce-google-sso.ts new file mode 100644 index 000000000..ce77a8141 --- /dev/null +++ b/backend/src/db/migrations/20250813214709_enforce-google-sso.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced"; +const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed"; +export async function up(knex: Knex): Promise { + const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn( + TableName.Organization, + GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME + ); + const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn( + TableName.Organization, + GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME + ); + + await knex.schema.alterTable(TableName.Organization, (table) => { + if (!hasGoogleSsoAuthEnforcedColumn) table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false); + if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable(); + }); +} + +export async function down(knex: Knex): Promise { + const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn( + TableName.Organization, + GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME + ); + + const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn( + TableName.Organization, + GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME + ); + + await knex.schema.alterTable(TableName.Organization, (table) => { + if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME); + if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME); + }); +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index fb0728707..449e6c493 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({ scannerProductEnabled: z.boolean().default(true).nullable().optional(), shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(), maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), - maxSharedSecretViewLimit: z.number().nullable().optional() + maxSharedSecretViewLimit: z.number().nullable().optional(), + googleSsoAuthEnforced: z.boolean().default(false).nullable().optional(), + googleSsoAuthLastUsed: z.date().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 098d00feb..345e26638 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -47,6 +47,7 @@ export type TFeatureSet = { auditLogStreamLimit: 3; githubOrgSync: false; samlSSO: false; + enforceGoogleSSO: false; hsm: false; oidcSSO: false; secretAccessInsights: false; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 9677c69b1..18c7e6f09 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -35,6 +35,7 @@ export interface TPermissionDALFactory { projectFavorites?: string[] | null | undefined; customRoleSlug?: string | null | undefined; orgAuthEnforced?: boolean | null | undefined; + orgGoogleSsoAuthEnforced?: boolean | null | undefined; } & { groups: { id: string; @@ -87,6 +88,7 @@ export interface TPermissionDALFactory { }[]; orgId: string; orgAuthEnforced: boolean | null | undefined; + orgGoogleSsoAuthEnforced: boolean | null | undefined; orgRole: OrgMembershipRole; userId: string; projectId: string; @@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), db.ref("groupId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"), @@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { OrgMembershipsSchema.extend({ permissions: z.unknown(), orgAuthEnforced: z.boolean().optional().nullable(), + orgGoogleSsoAuthEnforced: z.boolean().optional().nullable(), bypassOrgAuthEnabled: z.boolean(), customRoleSlug: z.string().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean() @@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), db.ref("orgId").withSchema(TableName.Project), @@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { orgId, username, orgAuthEnforced, + orgGoogleSsoAuthEnforced, orgRole, membershipId, groupMembershipId, @@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { }) => ({ orgId, orgAuthEnforced, + orgGoogleSsoAuthEnforced, orgRole: orgRole as OrgMembershipRole, userId, projectId, diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index d645e2bec..7d61d7499 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { function validateOrgSSO( actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"], + isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"], isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"], orgRole: OrgMembershipRole ) { @@ -128,10 +129,16 @@ function validateOrgSSO( throw new UnauthorizedError({ name: "No auth method defined" }); } - if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { + if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { return; } + // case: google sso is enforced, but the actor is not using google sso + if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) { + throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" }); + } + + // case: SAML SSO is enforced, but the actor is not using SAML SSO if ( isOrgSsoEnforced && actorAuthMethod !== null && diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 85ee82cca..461ceef46 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -146,6 +146,7 @@ export const permissionServiceFactory = ({ validateOrgSSO( authMethod, membership.orgAuthEnforced, + membership.orgGoogleSsoAuthEnforced, membership.bypassOrgAuthEnabled, membership.role as OrgMembershipRole ); @@ -238,6 +239,7 @@ export const permissionServiceFactory = ({ validateOrgSSO( authMethod, userProjectPermission.orgAuthEnforced, + userProjectPermission.orgGoogleSsoAuthEnforced, userProjectPermission.bypassOrgAuthEnabled, userProjectPermission.orgRole ); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 323354bc1..b8eb3ad6b 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { name: GenericResourceNameSchema.optional(), slug: slugSchema({ max: 64 }).optional(), authEnforced: z.boolean().optional(), + googleSsoAuthEnforced: z.boolean().optional(), scimEnabled: z.boolean().optional(), defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(), enforceMfa: z.boolean().optional(), diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 0aec39f3e..366fa331d 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { try { // @ts-expect-error this is because this is express type and not fastify const callbackPort = req.session.get("callbackPort"); + // @ts-expect-error this is because this is express type and not fastify + const orgSlug = req.session.get("orgSlug"); const email = profile?.emails?.[0]?.value; if (!email) @@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { firstName: profile?.name?.givenName || "", lastName: profile?.name?.familyName || "", authMethod: AuthMethod.GOOGLE, - callbackPort + callbackPort, + orgSlug }); cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { schema: { querystring: z.object({ callback_port: z.string().optional(), + org_slug: z.string().optional(), is_admin_login: z .string() .optional() @@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { }, preValidation: [ async (req, res) => { - const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query; + const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query; // ensure fresh session state per login attempt await req.session.regenerate(); if (callbackPort) { req.session.set("callbackPort", callbackPort); } + if (orgSlug) { + req.session.set("orgSlug", orgSlug); + } if (isAdminLogin) { req.session.set("isAdminLogin", isAdminLogin); } diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 6a3456508..6ef40b3c5 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -448,15 +448,34 @@ export const authLoginServiceFactory = ({ // Check if the user actually has access to the specified organization. const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); - const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited"); + + const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited"); + const selectedOrg = await orgDAL.findById(organizationId); - if (!hasOrganizationMembership) { + if (!selectedOrgMembership) { throw new ForbiddenRequestError({ message: `User does not have access to the organization named ${selectedOrg?.name}` }); } + if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) { + const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === 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(selectedOrg.id, { + googleSsoAuthLastUsed: new Date() + }); + } + const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; @@ -502,7 +521,8 @@ export const authLoginServiceFactory = ({ selectedOrg.authEnforced && selectedOrg.bypassOrgAuthEnabled && !isAuthMethodSaml(decodedToken.authMethod) && - decodedToken.authMethod !== AuthMethod.OIDC + decodedToken.authMethod !== AuthMethod.OIDC && + decodedToken.authMethod !== AuthMethod.GOOGLE ) { await auditLogService.createAuditLog({ orgId: organizationId, @@ -705,7 +725,7 @@ export const authLoginServiceFactory = ({ /* * OAuth2 login for google,github, and other oauth2 provider * */ - const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => { + const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => { // akhilmhdh: case sensitive email resolution const usersByUsername = await userDAL.findUserByUsername(email); let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0]; @@ -759,6 +779,8 @@ export const authLoginServiceFactory = ({ const appCfg = getConfig(); + let orgId = ""; + let orgName: undefined | string; if (!user) { // Create a new user based on oAuth if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" }); @@ -784,7 +806,6 @@ export const authLoginServiceFactory = ({ }); if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) { - let orgId = ""; const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId); if (!defaultOrg) { throw new BadRequestError({ @@ -824,11 +845,39 @@ export const authLoginServiceFactory = ({ } } + if (!orgId && orgSlug) { + const org = await orgDAL.findOrgBySlug(orgSlug); + + if (org) { + // checks for the membership and only sets the orgId / orgName if the user is a member of the specified org + const orgMembership = await orgDAL.findMembership({ + [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.OrgMembership}.orgId` as "orgId"]: org.id, + [`${TableName.OrgMembership}.isActive` as "isActive"]: true, + [`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted + }); + + if (orgMembership) { + orgId = org.id; + orgName = org.name; + } + } + } + const isUserCompleted = user.isAccepted; const providerAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, + + ...(orgId && orgSlug && orgName !== undefined + ? { + organizationId: orgId, + organizationName: orgName, + organizationSlug: orgSlug + } + : {}), + username: user.username, email: user.email, isEmailVerified: user.isEmailVerified, diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index d9d9520a8..09d81033f 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -32,6 +32,7 @@ export type TOauthLoginDTO = { lastName?: string; authMethod: AuthMethod; callbackPort?: string; + orgSlug?: string; }; export type TOauthTokenExchangeDTO = { diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts index ae82cd1bc..4a3bdb06e 100644 --- a/backend/src/services/org/org-schema.ts +++ b/backend/src/services/org/org-schema.ts @@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({ createdAt: true, updatedAt: true, authEnforced: true, + googleSsoAuthEnforced: true, scimEnabled: true, kmsDefaultKeyId: true, defaultMembershipRole: true, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5779fa7d3..d3312bca7 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -355,6 +355,7 @@ export const orgServiceFactory = ({ name, slug, authEnforced, + googleSsoAuthEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, @@ -421,6 +422,15 @@ export const orgServiceFactory = ({ } } + if (googleSsoAuthEnforced !== undefined) { + if (!plan.enforceGoogleSSO) { + throw new BadRequestError({ + message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO." + }); + } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + } + if (authEnforced) { const samlCfg = await samlConfigDAL.findOne({ orgId, @@ -451,6 +461,33 @@ export const orgServiceFactory = ({ } } + if (googleSsoAuthEnforced || authEnforced) { + if (googleSsoAuthEnforced && authEnforced) { + throw new BadRequestError({ + message: "Google SSO and SAML/OIDC auth enforcement cannot be enabled at the same time." + }); + } + + if (googleSsoAuthEnforced && currentOrg.authEnforced) { + throw new BadRequestError({ + message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled." + }); + } + + if (authEnforced && currentOrg.googleSsoAuthEnforced) { + throw new BadRequestError({ + message: "SAML/OIDC auth enforcement cannot be enabled when Google SSO auth enforcement is enabled." + }); + } + + if (!currentOrg.googleSsoAuthLastUsed) { + throw new BadRequestError({ + message: + "Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization." + }); + } + } + let defaultMembershipRole: string | undefined; if (defaultMembershipRoleSlug) { defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({ @@ -465,6 +502,7 @@ export const orgServiceFactory = ({ name, slug: slug ? slugify(slug) : undefined, authEnforced, + googleSsoAuthEnforced, scimEnabled, defaultMembershipRole, enforceMfa, diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 645692145..1a27d131f 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -74,6 +74,7 @@ export type TUpdateOrgDTO = { name: string; slug: string; authEnforced: boolean; + googleSsoAuthEnforced: boolean; scimEnabled: boolean; defaultMembershipRoleSlug: string; enforceMfa: boolean; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index cd620bb64..15e1b861c 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -104,6 +104,7 @@ export const useUpdateOrg = () => { mutationFn: ({ name, authEnforced, + googleSsoAuthEnforced, scimEnabled, slug, orgId, @@ -125,6 +126,7 @@ export const useUpdateOrg = () => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, authEnforced, + googleSsoAuthEnforced, scimEnabled, slug, defaultMembershipRoleSlug, diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 73267f0f6..71f2a8b90 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -9,6 +9,7 @@ export type Organization = { createAt: string; updatedAt: string; authEnforced: boolean; + googleSsoAuthEnforced: boolean; bypassOrgAuthEnabled: boolean; orgAuthMethod: string; scimEnabled: boolean; @@ -34,6 +35,7 @@ export type UpdateOrgDTO = { orgId: string; name?: string; authEnforced?: boolean; + googleSsoAuthEnforced?: boolean; scimEnabled?: boolean; slug?: string; defaultMembershipRoleSlug?: string; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 4ded71cfe..338599fe0 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -48,6 +48,7 @@ export type SubscriptionPlan = { externalKms: boolean; pkiEst: boolean; enforceMfa: boolean; + enforceGoogleSSO: boolean; projectTemplates: boolean; kmip: boolean; secretScanning: boolean; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index a46578726..301632860 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -238,6 +238,11 @@ export const Navbar = () => { } window.close(); return; + } else if (org.googleSsoAuthEnforced) { + await logout.mutateAsync(); + window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); + window.close(); + return; } handleOrgChange(org?.id); diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index f331e4bda..f978dd632 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -82,25 +82,42 @@ export const SelectOrganizationSection = () => { } } - if (organization.authEnforced && !canBypassOrgAuth) { + if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) { + const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod }; + + await new Promise((resolve) => setTimeout(resolve, 5_000)); + // org has an org-level auth method enabled (e.g. SAML) // -> logout + redirect to SAML SSO - await logout.mutateAsync(); let url = ""; if (organization.orgAuthMethod === AuthMethod.OIDC) { url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${ callbackPort ? `&callbackPort=${callbackPort}` : "" }`; - } else { + } else if (organization.orgAuthMethod === AuthMethod.SAML) { url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`; if (callbackPort) { url += `?callback_port=${callbackPort}`; } + } else if ( + organization.googleSsoAuthEnforced && + authToken.authMethod !== AuthMethod.GOOGLE + ) { + url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`; + + if (callbackPort) { + url += `&callback_port=${callbackPort}`; + } } - window.location.href = url; - return; + // we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO + // see line 103-106 + if (url) { + await logout.mutateAsync(); + window.location.href = url; + return; + } } const { token, isMfaEnabled, mfaMethod } = await selectOrg @@ -198,6 +215,8 @@ export const SelectOrganizationSection = () => { handleCliRedirect(); setIsInitialOrgCheckLoading(false); } else { + console.log(organizations.data); + console.log("Calling this with single org?!??!?!::::", organizations.data.length); handleSelectOrganization(organizations.data[0]); } } else { @@ -207,6 +226,7 @@ export const SelectOrganizationSection = () => { useEffect(() => { if (defaultSelectedOrg) { + console.log("Calling this with default org?!??!?!::::", defaultSelectedOrg); handleSelectOrganization(defaultSelectedOrg); } }, [defaultSelectedOrg]); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index ac8685192..406da5992 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -13,8 +13,21 @@ import { } from "@app/context"; import { useLogoutUser, useUpdateOrg } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { twMerge } from "tailwind-merge"; -export const OrgGeneralAuthSection = () => { +enum EnforceAuthType { + SAML = "saml", + GOOGLE = "google", + OIDC = "oidc" +} + +export const OrgGeneralAuthSection = ({ + isSamlConfigured, + isOidcConfigured +}: { + isSamlConfigured: boolean; + isOidcConfigured: boolean; +}) => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -23,27 +36,61 @@ export const OrgGeneralAuthSection = () => { const logout = useLogoutUser(); - const handleEnforceOrgAuthToggle = async (value: boolean) => { + const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => { try { if (!currentOrg?.id) return; - if (!subscription?.samlSSO) { - handlePopUpOpen("upgradePlan"); - return; + + if (type === EnforceAuthType.SAML) { + if (!subscription?.samlSSO) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgId: currentOrg?.id, + authEnforced: value + }); + } else if (type === EnforceAuthType.GOOGLE) { + if (!subscription?.enforceGoogleSSO) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgId: currentOrg?.id, + googleSsoAuthEnforced: value + }); + } else if (type === EnforceAuthType.OIDC) { + if (!subscription?.oidcSSO) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgId: currentOrg?.id, + authEnforced: value + }); + } else { + createNotification({ + text: `Invalid auth enforcement type ${type}`, + type: "error" + }); } - await mutateAsync({ - orgId: currentOrg?.id, - authEnforced: value - }); - createNotification({ - text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`, + text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`, type: "success" }); if (value) { await logout.mutateAsync(); - window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`); + + if (type === EnforceAuthType.SAML) { + window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`); + } else if (type === EnforceAuthType.GOOGLE) { + window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`); + } + window.close(); } } catch (err) { @@ -79,44 +126,78 @@ export const OrgGeneralAuthSection = () => { return ( <> - {/*
-
-

Allow users to send invites

- - {(isAllowed) => ( - handleEnforceOrgAuthToggle(value)} - isChecked={currentOrg?.authEnforced ?? false} - isDisabled={!isAllowed} - /> - )} - -
-

Allow members to invite new users to this organization

-
*/} -
-
-
- Enforce SAML SSO +
+
+
+
+ Enforce SAML SSO +
+ + {(isAllowed) => ( + + handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML) + } + isChecked={currentOrg?.authEnforced ?? false} + isDisabled={!isAllowed || currentOrg?.googleSsoAuthEnforced} + /> + )} +
- - {(isAllowed) => ( - handleEnforceOrgAuthToggle(value)} - isChecked={currentOrg?.authEnforced ?? false} - isDisabled={!isAllowed} - /> - )} - +

+ Enforce users to authenticate via SAML to access this organization. +

+
+ +
+
+
+ Enforce OIDC SSO +
+ + {(isAllowed) => ( + + handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC) + } + isDisabled={!isAllowed} + /> + )} + +
+

+ Enforce users to authenticate via OIDC to access this organization. +

+
+ +
+
+
+ Enforce Google SSO +
+ + {(isAllowed) => ( + + handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE) + } + isChecked={currentOrg?.googleSsoAuthEnforced ?? false} + isDisabled={!isAllowed || currentOrg?.authEnforced} + /> + )} + +
+

+ Enforce users to authenticate via Google to access this organization. +

-

- Enforce users to authenticate via SAML to access this organization -

- {currentOrg?.authEnforced && ( -
+ {(currentOrg?.authEnforced || currentOrg?.googleSsoAuthEnforced) && ( +
Enable Admin SSO Bypass @@ -125,8 +206,8 @@ export const OrgGeneralAuthSection = () => { content={
- When this is enabled, we strongly recommend enforcing MFA at the organization - level. + When enabling admin SSO bypass, we highly recommend enabling MFA enforcement + at the organization-level for security reasons.

In case of a lockout, admins can use the{" "} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx index 3956ba894..f91c2623a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx @@ -11,7 +11,7 @@ import { useOrganization, useSubscription } from "@app/context"; -import { useGetOIDCConfig, useLogoutUser, useUpdateOrg } from "@app/hooks/api"; +import { useGetOIDCConfig } from "@app/hooks/api"; import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -23,9 +23,7 @@ export const OrgOIDCSection = (): JSX.Element => { const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? ""); const { mutateAsync } = useUpdateOIDCConfig(); - const { mutateAsync: updateOrg } = useUpdateOrg(); - const logout = useLogoutUser(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addOIDC", "upgradePlan" @@ -54,56 +52,6 @@ export const OrgOIDCSection = (): JSX.Element => { } }; - const handleEnforceOrgAuthToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.oidcSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await updateOrg({ - orgId: currentOrg?.id, - authEnforced: value - }); - - createNotification({ - text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`, - type: "success" - }); - - if (value) { - await logout.mutateAsync(); - window.open(`/api/v1/sso/oidc/login?orgSlug=${currentOrg.slug}`); - window.close(); - } - } catch (err) { - console.error(err); - } - }; - - const handleEnableBypassOrgAuthToggle = async (value: boolean) => { - try { - if (!currentOrg?.id) return; - if (!subscription?.oidcSSO) { - handlePopUpOpen("upgradePlan"); - return; - } - - await updateOrg({ - orgId: currentOrg?.id, - bypassOrgAuthEnabled: value - }); - - createNotification({ - text: `Successfully ${value ? "enabled" : "disabled"} admin bypassing of org-level auth`, - type: "success" - }); - } catch (err) { - console.error(err); - } - }; - const handleOIDCGroupManagement = async (value: boolean) => { try { if (!currentOrg?.id) return; @@ -178,88 +126,6 @@ export const OrgOIDCSection = (): JSX.Element => {

)} -
-
-
- Enforce OIDC SSO -
- - {(isAllowed) => ( - handleEnforceOrgAuthToggle(value)} - isDisabled={!isAllowed} - /> - )} - -
-

- Enforce users to authenticate via OIDC to access this organization. -

-
- {currentOrg?.authEnforced && ( -
-
-
- Enable Admin SSO Bypass - - - When this is enabled, we strongly recommend enforcing MFA at the organization - level. - -

- In case of a lockout, admins can use the{" "} - - Admin Login Portal - {" "} - at{" "} - - {window.location.origin}/login/admin - -

-
- } - > - - -
- - {(isAllowed) => ( - handleEnableBypassOrgAuthToggle(value)} - isDisabled={!isAllowed} - /> - )} - -
-

- - Allow organization admins to bypass OIDC enforcement when SSO is unavailable, - misconfigured, or inaccessible. - -

-
- )}
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx index 3ffd8b3c2..4b1278b6d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx @@ -79,10 +79,9 @@ export const OrgSSOSection = (): JSX.Element => { }; return ( - <> -
-
-
+
+
+

SAML

{!isPending && ( @@ -96,7 +95,7 @@ export const OrgSSOSection = (): JSX.Element => {

Manage SAML authentication configuration

-
+

Enable SAML

{!isPending && ( @@ -126,6 +125,6 @@ export const OrgSSOSection = (): JSX.Element => { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text="You can use SAML SSO if you switch to Infisical's Pro plan." /> - +
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx index 9b11c1302..2d8e0af27 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -49,10 +49,15 @@ export const OrgSsoTab = withPermission( ); const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig; - const shouldDisplaySection = (method: LoginMethod) => - !enabledLoginMethods || enabledLoginMethods.includes(method); + const shouldDisplaySection = (method: LoginMethod[] | LoginMethod) => { + if (Array.isArray(method)) { + return method.some((m) => !enabledLoginMethods || enabledLoginMethods.includes(m)); + } - const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer); + return !enabledLoginMethods || enabledLoginMethods.includes(method); + }; + + const isOidcConfigured = Boolean(oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer)); const isSamlConfigured = samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert); const isLdapConfigured = ldapConfig && ldapConfig.url; @@ -65,10 +70,11 @@ export const OrgSsoTab = withPermission( shouldDisplaySection(LoginMethod.OIDC) || shouldDisplaySection(LoginMethod.LDAP) ? ( <> -
+

Connect an Identity Provider

- Connect your identity provider to simplify user management + Connect your identity provider to simplify user management with options like SAML, + OIDC, and LDAP.

{shouldDisplaySection(LoginMethod.SAML) && (
{shouldShowCreateIdentityProviderView ? ( - createIdentityProviderView +
+ +
+ {createIdentityProviderView} +
) : ( - <> - {isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && ( -
- - -
- )} - {isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && } - {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } - +
+
+ {/* {shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && ( */} + + {/* )} */} +
+
+
+ {isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && } + {isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && } + {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } +
+
)} Date: Thu, 14 Aug 2025 06:15:25 +0400 Subject: [PATCH 2/5] Update license-fns.ts --- backend/src/ee/services/license/license-fns.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index bd3949a7e..8d2d6fdbe 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogStreams: false, auditLogStreamLimit: 3, samlSSO: false, + enforceGoogleSSO: false, hsm: false, oidcSSO: false, scim: false, From a37f1eb1f8a20dc307c7b1f2ada4e3371e4e8a3d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 14 Aug 2025 06:53:57 +0400 Subject: [PATCH 3/5] requested changes & frontend lint --- backend/src/@types/fastify.d.ts | 2 +- .../20250813214709_enforce-google-sso.ts | 3 ++- backend/src/db/schemas/organizations.ts | 2 +- backend/src/services/org/org-service.ts | 20 +++++++------------ .../components/NavBar/Navbar.tsx | 4 +++- .../auth/SelectOrgPage/SelectOrgSection.tsx | 5 ----- .../OrgSsoTab/OrgGeneralAuthSection.tsx | 4 ++-- 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f65f8a4fa..c25d8d4d1 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -148,7 +148,7 @@ declare module "fastify" { interface Session { callbackPort: string; isAdminLogin: boolean; - orgSlug: string; + orgSlug?: string; } interface FastifyRequest { diff --git a/backend/src/db/migrations/20250813214709_enforce-google-sso.ts b/backend/src/db/migrations/20250813214709_enforce-google-sso.ts index ce77a8141..2346c91a4 100644 --- a/backend/src/db/migrations/20250813214709_enforce-google-sso.ts +++ b/backend/src/db/migrations/20250813214709_enforce-google-sso.ts @@ -15,7 +15,8 @@ export async function up(knex: Knex): Promise { ); await knex.schema.alterTable(TableName.Organization, (table) => { - if (!hasGoogleSsoAuthEnforcedColumn) table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false); + if (!hasGoogleSsoAuthEnforcedColumn) + table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable(); if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable(); }); } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 449e6c493..afc9e2b73 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -37,7 +37,7 @@ export const OrganizationsSchema = z.object({ shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(), maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(), - googleSsoAuthEnforced: z.boolean().default(false).nullable().optional(), + googleSsoAuthEnforced: z.boolean().default(false), googleSsoAuthLastUsed: z.date().nullable().optional() }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index d3312bca7..610dac4d1 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -431,6 +431,12 @@ export const orgServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); } + if (authEnforced && googleSsoAuthEnforced) { + throw new BadRequestError({ + message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time." + }); + } + if (authEnforced) { const samlCfg = await samlConfigDAL.findOne({ orgId, @@ -461,25 +467,13 @@ export const orgServiceFactory = ({ } } - if (googleSsoAuthEnforced || authEnforced) { - if (googleSsoAuthEnforced && authEnforced) { - throw new BadRequestError({ - message: "Google SSO and SAML/OIDC auth enforcement cannot be enabled at the same time." - }); - } - + if (googleSsoAuthEnforced) { if (googleSsoAuthEnforced && currentOrg.authEnforced) { throw new BadRequestError({ message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled." }); } - if (authEnforced && currentOrg.googleSsoAuthEnforced) { - throw new BadRequestError({ - message: "SAML/OIDC auth enforcement cannot be enabled when Google SSO auth enforcement is enabled." - }); - } - if (!currentOrg.googleSsoAuthLastUsed) { throw new BadRequestError({ message: diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 301632860..5c37d4404 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -238,7 +238,9 @@ export const Navbar = () => { } window.close(); return; - } else if (org.googleSsoAuthEnforced) { + } + + if (org.googleSsoAuthEnforced) { await logout.mutateAsync(); window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); window.close(); diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index f978dd632..9e6850f82 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -85,8 +85,6 @@ export const SelectOrganizationSection = () => { if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) { const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod }; - await new Promise((resolve) => setTimeout(resolve, 5_000)); - // org has an org-level auth method enabled (e.g. SAML) // -> logout + redirect to SAML SSO let url = ""; @@ -215,8 +213,6 @@ export const SelectOrganizationSection = () => { handleCliRedirect(); setIsInitialOrgCheckLoading(false); } else { - console.log(organizations.data); - console.log("Calling this with single org?!??!?!::::", organizations.data.length); handleSelectOrganization(organizations.data[0]); } } else { @@ -226,7 +222,6 @@ export const SelectOrganizationSection = () => { useEffect(() => { if (defaultSelectedOrg) { - console.log("Calling this with default org?!??!?!::::", defaultSelectedOrg); handleSelectOrganization(defaultSelectedOrg); } }, [defaultSelectedOrg]); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index 406da5992..262f9e2c0 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -135,7 +135,7 @@ export const OrgGeneralAuthSection = ({ {(isAllowed) => ( handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML) } @@ -158,7 +158,7 @@ export const OrgGeneralAuthSection = ({ {(isAllowed) => ( handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC) From 09db98db50ba9c853e54a69cd4ae439a5c5bc5cc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 14 Aug 2025 06:58:45 +0400 Subject: [PATCH 4/5] fix: typescript complaining --- backend/src/ee/services/permission/permission-dal.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 18c7e6f09..cdf55127f 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -35,7 +35,7 @@ export interface TPermissionDALFactory { projectFavorites?: string[] | null | undefined; customRoleSlug?: string | null | undefined; orgAuthEnforced?: boolean | null | undefined; - orgGoogleSsoAuthEnforced?: boolean | null | undefined; + orgGoogleSsoAuthEnforced: boolean; } & { groups: { id: string; @@ -88,7 +88,7 @@ export interface TPermissionDALFactory { }[]; orgId: string; orgAuthEnforced: boolean | null | undefined; - orgGoogleSsoAuthEnforced: boolean | null | undefined; + orgGoogleSsoAuthEnforced: boolean; orgRole: OrgMembershipRole; userId: string; projectId: string; @@ -372,7 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { OrgMembershipsSchema.extend({ permissions: z.unknown(), orgAuthEnforced: z.boolean().optional().nullable(), - orgGoogleSsoAuthEnforced: z.boolean().optional().nullable(), + orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean(), customRoleSlug: z.string().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean() From d587e779f547762f557ccdd0ccaf8e8331812cf7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 16 Aug 2025 00:26:06 +0400 Subject: [PATCH 5/5] requested changes --- .../OrgSsoTab/OrgGeneralAuthSection.tsx | 26 +++++++-- .../components/OrgSsoTab/OrgLDAPSection.tsx | 55 ++++++++++--------- .../components/OrgSsoTab/OrgOIDCSection.tsx | 33 +++++------ .../components/OrgSsoTab/OrgSSOSection.tsx | 28 +++++----- .../components/OrgSsoTab/OrgSsoTab.tsx | 53 +++++++++--------- 5 files changed, 103 insertions(+), 92 deletions(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index 262f9e2c0..de568931a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -23,10 +23,12 @@ enum EnforceAuthType { export const OrgGeneralAuthSection = ({ isSamlConfigured, - isOidcConfigured + isOidcConfigured, + isGoogleConfigured }: { isSamlConfigured: boolean; isOidcConfigured: boolean; + isGoogleConfigured: boolean; }) => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); @@ -125,8 +127,14 @@ export const OrgGeneralAuthSection = ({ }; return ( - <> -
+
+
+

SSO Enforcement

+

+ Manage strict enforcement of specific authentication methods for your organization. +

+
+
@@ -147,6 +155,8 @@ export const OrgGeneralAuthSection = ({

Enforce users to authenticate via SAML to access this organization. +
+ When this is enabled your organization members will only be able to login with SAML.

@@ -169,11 +179,13 @@ export const OrgGeneralAuthSection = ({

- Enforce users to authenticate via OIDC to access this organization. + Enforce users to authenticate via OIDC to access this organization. +
+ When this is enabled your organization members will only be able to login with OIDC.

-
+
Enforce Google SSO @@ -193,6 +205,8 @@ export const OrgGeneralAuthSection = ({

Enforce users to authenticate via Google to access this organization. +
+ When this is enabled your organization members will only be able to login with Google.

@@ -263,6 +277,6 @@ export const OrgGeneralAuthSection = ({ onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text="You can enforce SAML SSO if you switch to Infisical's Pro plan." /> - +
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx index 9c7293210..e66987bae 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx @@ -95,43 +95,25 @@ export const OrgLDAPSection = (): JSX.Element => { }; return ( -
+
-
-

LDAP

-
- - {(isAllowed) => ( - - )} - +
+
+

LDAP

+

Manage LDAP authentication configuration

-
-

Manage LDAP authentication configuration

-
-
-
-

LDAP Group Mappings

{(isAllowed) => ( - )}
-

- Manage how LDAP groups are mapped to internal groups in Infisical -

+ {data && ( -
+

Enable LDAP

@@ -152,6 +134,27 @@ export const OrgLDAPSection = (): JSX.Element => {

)} + +
+
+

LDAP Group Mappings

+ + {(isAllowed) => ( + + )} + +
+

+ Manage how LDAP groups are mapped to internal groups in Infisical +

+
+ { }; return ( -
-
-
-

OIDC

- {!isPending && ( - - {(isAllowed) => ( - - )} - - )} +
+
+
+

OIDC

+

Manage OIDC authentication configuration

-

Manage OIDC authentication configuration

+ + {!isPending && ( + + {(isAllowed) => ( + + )} + + )}
{data && (
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx index 4b1278b6d..33843f50f 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx @@ -80,23 +80,23 @@ export const OrgSSOSection = (): JSX.Element => { return (
-
-
-

SAML

- {!isPending && ( - - {(isAllowed) => ( - - )} - - )} +
+
+

SAML

+

Manage SAML authentication configuration

-

Manage SAML authentication configuration

+ {!isPending && ( + + {(isAllowed) => ( + + )} + + )}
-
+

Enable SAML

{!isPending && ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx index 2d8e0af27..9964fdf4d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -61,6 +61,7 @@ export const OrgSsoTab = withPermission( const isSamlConfigured = samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert); const isLdapConfigured = ldapConfig && ldapConfig.url; + const isGoogleConfigured = shouldDisplaySection(LoginMethod.GOOGLE); const shouldShowCreateIdentityProviderView = !isOidcConfigured && !isSamlConfigured && !isLdapConfigured; @@ -70,12 +71,14 @@ export const OrgSsoTab = withPermission( shouldDisplaySection(LoginMethod.OIDC) || shouldDisplaySection(LoginMethod.LDAP) ? ( <> -
-

Connect an Identity Provider

-

- Connect your identity provider to simplify user management with options like SAML, - OIDC, and LDAP. -

+
+
+

Connect an Identity Provider

+

+ Connect your identity provider to simplify user management with options like SAML, + OIDC, and LDAP. +

+
{shouldDisplaySection(LoginMethod.SAML) && (
- {shouldShowCreateIdentityProviderView ? ( -
+
+ {shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && ( -
- {createIdentityProviderView} -
- ) : ( -
-
- {/* {shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && ( */} - - {/* )} */} + )} + + {shouldShowCreateIdentityProviderView ? ( + createIdentityProviderView + ) : ( +
+
+ {isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && } + {isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && } + {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } +
-
-
- {isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && } - {isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && } - {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } -
-
- )} + )} +
handlePopUpToggle("upgradePlan", isOpen)}