diff --git a/backend/src/db/migrations/20240930072738_add-oidc-auth-enforced-to-org.ts b/backend/src/db/migrations/20240930072738_add-oidc-auth-enforced-to-org.ts new file mode 100644 index 000000000..284ac7050 --- /dev/null +++ b/backend/src/db/migrations/20240930072738_add-oidc-auth-enforced-to-org.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed"))) { + await knex.schema.alterTable(TableName.OidcConfig, (tb) => { + tb.datetime("lastUsed"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) { + await knex.schema.alterTable(TableName.OidcConfig, (tb) => { + tb.dropColumn("lastUsed"); + }); + } +} diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index d78f9e9a7..e8030267d 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({ isActive: z.boolean(), createdAt: z.date(), updatedAt: z.date(), - orgId: z.string().uuid() + orgId: z.string().uuid(), + lastUsed: z.date().nullable().optional() }); export type TOidcConfigs = z.infer; diff --git a/backend/src/ee/services/oidc/oidc-config-dal.ts b/backend/src/ee/services/oidc/oidc-config-dal.ts index 470916c61..ffdba2cf7 100644 --- a/backend/src/ee/services/oidc/oidc-config-dal.ts +++ b/backend/src/ee/services/oidc/oidc-config-dal.ts @@ -1,5 +1,6 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TOidcConfigDALFactory = ReturnType; @@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType; export const oidcConfigDALFactory = (db: TDbClient) => { const oidcCfgOrm = ormify(db, TableName.OidcConfig); - return { ...oidcCfgOrm }; + const findEnforceableOidcCfg = async (orgId: string) => { + try { + const oidcCfg = await db + .replicaNode()(TableName.OidcConfig) + .where({ + orgId, + isActive: true + }) + .whereNotNull("lastUsed") + .first(); + + return oidcCfg; + } catch (error) { + throw new DatabaseError({ error, name: "Find org by id" }); + } + }; + + return { ...oidcCfgOrm, findEnforceableOidcCfg }; }; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index f86f29646..983296539 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({ } ); + await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() }); + if (user.email && !user.isEmailVerified) { const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_VERIFICATION, @@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, jwksUri, - isActive + isActive, + lastUsed: null }; if (clientId !== undefined) { @@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({ } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); + await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false }); return ssoConfig; }; diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index eda19c215..ce6e3a88f 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { ].includes(actorAuthMethod); } -function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { +function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } - if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) { + if ( + isOrgSsoEnforced && + actorAuthMethod !== null && + !isAuthMethodSaml(actorAuthMethod) && + actorAuthMethod !== AuthMethod.OIDC + ) { throw new UnauthorizedError({ name: "Cannot access org-scoped resource" }); } } -export { isAuthMethodSaml, validateOrgSAML }; +export { isAuthMethodSaml, validateOrgSSO }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index cb1e84677..5b5fe4583 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -19,7 +19,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; -import { validateOrgSAML } from "./permission-fns"; +import { validateOrgSSO } from "./permission-fns"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types"; import { buildServiceTokenProjectPermission, @@ -130,7 +130,7 @@ export const permissionServiceFactory = ({ throw new UnauthorizedError({ name: "You are not logged into this organization" }); } - validateOrgSAML(authMethod, membership.orgAuthEnforced); + validateOrgSSO(authMethod, membership.orgAuthEnforced); const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( membership?.groups?.map(({ role, customRolePermission }) => ({ @@ -213,7 +213,7 @@ export const permissionServiceFactory = ({ throw new UnauthorizedError({ name: "You are not logged into this organization" }); } - validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced); + validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); // join two permissions and pass to build the final permission set const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5d9632215..4bdd92402 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -506,7 +506,8 @@ export const registerRoutes = async ( smtpService, userDAL, groupDAL, - orgBotDAL + orgBotDAL, + oidcConfigDAL }); const signupService = authSignupServiceFactory({ tokenService, diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 9e2f09821..b113b9f9d 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -28,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - organizations: OrganizationsSchema.array() + organizations: OrganizationsSchema.extend({ + orgAuthMethod: z.string() + }).array() }) } }, diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d84dd1afd..0093c4f69 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -29,13 +29,37 @@ export const orgDALFactory = (db: TDbClient) => { }; // special query - const findAllOrgsByUserId = async (userId: string): Promise => { + const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { try { - const org = await db + const org = (await db .replicaNode()(TableName.OrgMembership) .where({ userId }) .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) - .select(selectAllTableCols(TableName.Organization)); + .leftJoin(TableName.SamlConfig, (qb) => { + qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.SamlConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .leftJoin(TableName.OidcConfig, (qb) => { + qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.OidcConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .select(selectAllTableCols(TableName.Organization)) + .select( + db.raw(` + CASE + WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml' + WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc' + ELSE '' + END as "orgAuthMethod" + `) + )) as (TOrganizations & { orgAuthMethod: string })[]; + return org; } catch (error) { throw new DatabaseError({ error, name: "Find all org by user id" }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 8417f2444..2c349d744 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -18,6 +18,7 @@ import { import { TProjects } from "@app/db/schemas/projects"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -80,6 +81,7 @@ type TOrgServiceFactoryDep = { orgMembershipDAL: Pick; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; + oidcConfigDAL: Pick; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; @@ -114,6 +116,7 @@ export const orgServiceFactory = ({ licenseService, projectRoleDAL, samlConfigDAL, + oidcConfigDAL, projectBotDAL, projectUserMembershipRoleDAL }: TOrgServiceFactoryDep) => { @@ -266,10 +269,9 @@ export const orgServiceFactory = ({ const plan = await licenseService.getPlan(orgId); if (authEnforced !== undefined) { - if (!plan?.samlSSO) + if (!plan?.samlSSO || !plan.oidcSSO) throw new BadRequestError({ - message: - "Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO." + message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO." }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); } @@ -285,10 +287,12 @@ export const orgServiceFactory = ({ if (authEnforced) { const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); - if (!samlCfg) + const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId); + + if (!samlCfg && !oidcCfg) throw new BadRequestError({ - name: "No enforceable SAML config found", - message: "No enforceable SAML config found" + name: "No enforceable SSO config found", + message: "No enforceable SSO config found" }); } diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index 2b459d5ca..331eecdf1 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Manage**. + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. @@ -54,6 +54,19 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." ![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png) + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via Auth0. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Auth0 user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the application in Auth0 + prior to enforcing OIDC SSO to prevent any unintended issues. + + + diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index ae559cc59..b32bea8e5 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -28,7 +28,7 @@ Prerequisites: 1.4. Access the IdP’s OIDC discovery document (usually located at `https:///.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. - 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage + 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png) 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints. @@ -53,9 +53,20 @@ Prerequisites: Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider - ![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png) + ![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png) + - + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via the Identity provider. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one IdP user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues. + + diff --git a/docs/documentation/platform/sso/keycloak-oidc.mdx b/docs/documentation/platform/sso/keycloak-oidc.mdx index d3818f61c..d8bb8d306 100644 --- a/docs/documentation/platform/sso/keycloak-oidc.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc.mdx @@ -65,7 +65,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. @@ -80,6 +80,19 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via Keycloak. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the application in Keycloak + prior to enforcing OIDC SSO to prevent any unintended issues. + + + diff --git a/docs/images/sso/auth0-oidc/enable-oidc.png b/docs/images/sso/auth0-oidc/enable-oidc.png index 32682c302..0a43f22ed 100644 Binary files a/docs/images/sso/auth0-oidc/enable-oidc.png and b/docs/images/sso/auth0-oidc/enable-oidc.png differ diff --git a/docs/images/sso/auth0-oidc/org-oidc-overview.png b/docs/images/sso/auth0-oidc/org-oidc-overview.png index ce5fb0d60..f5778b97a 100644 Binary files a/docs/images/sso/auth0-oidc/org-oidc-overview.png and b/docs/images/sso/auth0-oidc/org-oidc-overview.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-enable.png b/docs/images/sso/general-oidc/org-oidc-enable.png index 69f0062bc..0a43f22ed 100644 Binary files a/docs/images/sso/general-oidc/org-oidc-enable.png and b/docs/images/sso/general-oidc/org-oidc-enable.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-manage.png b/docs/images/sso/general-oidc/org-oidc-manage.png index 69a60f0f5..f5778b97a 100644 Binary files a/docs/images/sso/general-oidc/org-oidc-manage.png and b/docs/images/sso/general-oidc/org-oidc-manage.png differ diff --git a/docs/images/sso/keycloak-oidc/enable-oidc.png b/docs/images/sso/keycloak-oidc/enable-oidc.png index 6518319c4..0a43f22ed 100644 Binary files a/docs/images/sso/keycloak-oidc/enable-oidc.png and b/docs/images/sso/keycloak-oidc/enable-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/manage-org-oidc.png b/docs/images/sso/keycloak-oidc/manage-org-oidc.png index e6346cd24..f5778b97a 100644 Binary files a/docs/images/sso/keycloak-oidc/manage-org-oidc.png and b/docs/images/sso/keycloak-oidc/manage-org-oidc.png differ diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx index 4a0d963cc..9f57c0332 100644 --- a/frontend/src/hooks/api/oidcConfig/mutations.tsx +++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { organizationKeys } from "../organization/queries"; import { oidcConfigKeys } from "./queries"; export const useUpdateOIDCConfig = () => { @@ -53,6 +54,7 @@ export const useUpdateOIDCConfig = () => { }, onSuccess(_, dto) { queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug)); + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); } }); }; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 684a53af4..cfda9343c 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -7,6 +7,7 @@ export type Organization = { createAt: string; updatedAt: string; authEnforced: boolean; + orgAuthMethod: string; scimEnabled: boolean; slug: string; }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index c96e3d735..ec0fa5f34 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -9,7 +9,8 @@ export enum AuthMethod { AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", KEYCLOAK_SAML = "keycloak-saml", - LDAP = "ldap" + LDAP = "ldap", + OIDC = "oidc" } export type User = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 382404c91..ccdc7c101 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -80,6 +80,7 @@ import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { Workspace } from "@app/hooks/api/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; +import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { CreateOrgModal } from "@app/views/Org/components"; @@ -385,9 +386,13 @@ export const AppLayout = ({ children }: LayoutProps) => { // -> logout + redirect to SAML SSO await logout.mutateAsync(); - window.open( - `/api/v1/sso/redirect/saml2/organizations/${org.slug}` - ); + if (org.orgAuthMethod === AuthMethod.OIDC) { + window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`); + } else { + window.open( + `/api/v1/sso/redirect/saml2/organizations/${org.slug}` + ); + } window.close(); return; } diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index 13ec3e414..5b9c0f72c 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -18,6 +18,7 @@ import { useUser } from "@app/context"; import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api"; import { UserAgentType } from "@app/hooks/api/auth/types"; import { Organization } from "@app/hooks/api/types"; +import { AuthMethod } from "@app/hooks/api/users/types"; import { getAuthToken, isLoggedIn } from "@app/reactQuery"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; @@ -57,14 +58,21 @@ export default function LoginPage() { if (organization.authEnforced) { // org has an org-level auth method enabled (e.g. SAML) // -> logout + redirect to SAML SSO - let samlUrl = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`; + await logout.mutateAsync(); + let url = ""; + if (organization.orgAuthMethod === AuthMethod.OIDC) { + url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${ + callbackPort ? `&callbackPort=${callbackPort}` : "" + }`; + } else { + url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`; - if (callbackPort) { - samlUrl += `?callback_port=${callbackPort}`; + if (callbackPort) { + url += `?callback_port=${callbackPort}`; + } } - await logout.mutateAsync(); - window.open(samlUrl); + window.open(url); window.close(); return; } diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx index 6ec56f7b3..d3b6be055 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx @@ -7,7 +7,7 @@ import { useOrganization, useSubscription } from "@app/context"; -import { useGetOIDCConfig } from "@app/hooks/api"; +import { useGetOIDCConfig, useLogoutUser, useUpdateOrg } from "@app/hooks/api"; import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -19,6 +19,9 @@ export const OrgOIDCSection = (): JSX.Element => { const { data, isLoading } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { mutateAsync } = useUpdateOIDCConfig(); + const { mutateAsync: updateOrg } = useUpdateOrg(); + + const logout = useLogoutUser(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addOIDC", "upgradePlan" @@ -51,6 +54,37 @@ export const OrgOIDCSection = (): JSX.Element => { } }; + const handleEnforceOrgAuthToggle = async (value: boolean) => { + try { + if (!currentOrg?.id) return; + if (!subscription?.samlSSO) { + 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) { + createNotification({ + text: (err as { response: { data: { message: string } } }).response.data.message, + type: "error" + }); + } + }; + const addOidcButtonClick = async () => { if (subscription?.oidcSSO && currentOrg) { handlePopUpOpen("addOIDC"); @@ -102,6 +136,24 @@ export const OrgOIDCSection = (): JSX.Element => {

)} +
+
+

Enforce OIDC SSO

+ + {(isAllowed) => ( + handleEnforceOrgAuthToggle(value)} + isDisabled={!isAllowed} + /> + )} + +
+

+ Enforce members to authenticate via OIDC to access this organization +

+