Merge pull request #2512 from Infisical/feat/enforce-oidc-sso
feat: enforce oidc sso
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
tb.dropColumn("lastUsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
|
|||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid()
|
orgId: z.string().uuid(),
|
||||||
|
lastUsed: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||||
@@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
|||||||
export const oidcConfigDALFactory = (db: TDbClient) => {
|
export const oidcConfigDALFactory = (db: TDbClient) => {
|
||||||
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !user.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
@@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
jwksUri,
|
jwksUri,
|
||||||
isActive
|
isActive,
|
||||||
|
lastUsed: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
||||||
|
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
|
||||||
return ssoConfig;
|
return ssoConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
].includes(actorAuthMethod);
|
].includes(actorAuthMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
|
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
|
||||||
if (actorAuthMethod === undefined) {
|
if (actorAuthMethod === undefined) {
|
||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
|
if (
|
||||||
throw new ForbiddenRequestError({ name: "SAML auth enforced, cannot access org-scoped resource" });
|
isOrgSsoEnforced &&
|
||||||
|
actorAuthMethod !== null &&
|
||||||
|
!isAuthMethodSaml(actorAuthMethod) &&
|
||||||
|
actorAuthMethod !== AuthMethod.OIDC
|
||||||
|
) {
|
||||||
|
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isAuthMethodSaml, validateOrgSAML };
|
export { isAuthMethodSaml, validateOrgSSO };
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
|
|
||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSAML } from "./permission-fns";
|
import { validateOrgSSO } from "./permission-fns";
|
||||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
@@ -130,7 +130,7 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ 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(
|
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||||
membership?.groups?.map(({ role, customRolePermission }) => ({
|
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||||
@@ -213,7 +213,7 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ 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
|
// join two permissions and pass to build the final permission set
|
||||||
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
|||||||
@@ -511,7 +511,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL
|
orgBotDAL,
|
||||||
|
oidcConfigDAL
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: OrganizationsSchema.extend({
|
||||||
|
orgAuthMethod: z.string()
|
||||||
|
}).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,13 +29,37 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => {
|
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
||||||
try {
|
try {
|
||||||
const org = await db
|
const org = (await db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
.where({ userId })
|
.where({ userId })
|
||||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
.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;
|
return org;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all org by user id" });
|
throw new DatabaseError({ error, name: "Find all org by user id" });
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -82,6 +83,7 @@ type TOrgServiceFactoryDep = {
|
|||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
||||||
incidentContactDAL: TIncidentContactsDALFactory;
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||||
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
@@ -116,6 +118,7 @@ export const orgServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
|
oidcConfigDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL
|
identityMetadataDAL
|
||||||
@@ -269,10 +272,9 @@ export const orgServiceFactory = ({
|
|||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
if (authEnforced !== undefined) {
|
if (authEnforced !== undefined) {
|
||||||
if (!plan?.samlSSO)
|
if (!plan?.samlSSO || !plan.oidcSSO)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO."
|
||||||
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
|
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
}
|
}
|
||||||
@@ -288,9 +290,11 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
if (authEnforced) {
|
if (authEnforced) {
|
||||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||||
if (!samlCfg)
|
const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId);
|
||||||
|
|
||||||
|
if (!samlCfg && !oidcCfg)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "No enforceable SAML config found"
|
message: "No enforceable SSO config found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
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**.
|
||||||

|

|
||||||
|
|
||||||
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.
|
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."
|
|||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in Auth0
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Prerequisites:
|
|||||||
1.4. Access the IdP’s OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
|
1.4. Access the IdP’s OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
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.
|
||||||

|

|
||||||
|
|
||||||
2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
|
2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
|
||||||
@@ -53,9 +53,20 @@ Prerequisites:
|
|||||||
<Step title="Enable OIDC SSO in Infisical">
|
<Step title="Enable OIDC SSO in Infisical">
|
||||||
Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
|
Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
|
||||||
|
|
||||||

|

|
||||||
|
</Step>
|
||||||
|
|
||||||
</Step>
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
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.
|
||||||

|

|
||||||
|
|
||||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
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."
|
|||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 754 KiB After Width: | Height: | Size: 797 KiB |
|
Before Width: | Height: | Size: 746 KiB After Width: | Height: | Size: 780 KiB |
|
Before Width: | Height: | Size: 743 KiB After Width: | Height: | Size: 797 KiB |
|
Before Width: | Height: | Size: 741 KiB After Width: | Height: | Size: 780 KiB |
|
Before Width: | Height: | Size: 751 KiB After Width: | Height: | Size: 797 KiB |
|
Before Width: | Height: | Size: 744 KiB After Width: | Height: | Size: 780 KiB |
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { organizationKeys } from "../organization/queries";
|
||||||
import { oidcConfigKeys } from "./queries";
|
import { oidcConfigKeys } from "./queries";
|
||||||
|
|
||||||
export const useUpdateOIDCConfig = () => {
|
export const useUpdateOIDCConfig = () => {
|
||||||
@@ -53,6 +54,7 @@ export const useUpdateOIDCConfig = () => {
|
|||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
|
queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type Organization = {
|
|||||||
createAt: string;
|
createAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
authEnforced: boolean;
|
authEnforced: boolean;
|
||||||
|
orgAuthMethod: string;
|
||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export enum AuthMethod {
|
|||||||
AZURE_SAML = "azure-saml",
|
AZURE_SAML = "azure-saml",
|
||||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||||
KEYCLOAK_SAML = "keycloak-saml",
|
KEYCLOAK_SAML = "keycloak-saml",
|
||||||
LDAP = "ldap"
|
LDAP = "ldap",
|
||||||
|
OIDC = "oidc"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
|||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
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 { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||||
import { CreateOrgModal } from "@app/views/Org/components";
|
import { CreateOrgModal } from "@app/views/Org/components";
|
||||||
|
|
||||||
@@ -385,9 +386,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
// -> logout + redirect to SAML SSO
|
// -> logout + redirect to SAML SSO
|
||||||
|
|
||||||
await logout.mutateAsync();
|
await logout.mutateAsync();
|
||||||
window.open(
|
if (org.orgAuthMethod === AuthMethod.OIDC) {
|
||||||
`/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
|
||||||
);
|
} else {
|
||||||
|
window.open(
|
||||||
|
`/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
||||||
|
);
|
||||||
|
}
|
||||||
window.close();
|
window.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useUser } from "@app/context";
|
|||||||
import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
|
import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||||
import { Organization } from "@app/hooks/api/types";
|
import { Organization } from "@app/hooks/api/types";
|
||||||
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||||
|
|
||||||
@@ -57,14 +58,21 @@ export default function LoginPage() {
|
|||||||
if (organization.authEnforced) {
|
if (organization.authEnforced) {
|
||||||
// org has an org-level auth method enabled (e.g. SAML)
|
// org has an org-level auth method enabled (e.g. SAML)
|
||||||
// -> logout + redirect to SAML SSO
|
// -> 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) {
|
if (callbackPort) {
|
||||||
samlUrl += `?callback_port=${callbackPort}`;
|
url += `?callback_port=${callbackPort}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await logout.mutateAsync();
|
window.open(url);
|
||||||
window.open(samlUrl);
|
|
||||||
window.close();
|
window.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} 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 { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -19,6 +19,9 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
|
|
||||||
const { data, isLoading } = useGetOIDCConfig(currentOrg?.slug ?? "");
|
const { data, isLoading } = useGetOIDCConfig(currentOrg?.slug ?? "");
|
||||||
const { mutateAsync } = useUpdateOIDCConfig();
|
const { mutateAsync } = useUpdateOIDCConfig();
|
||||||
|
const { mutateAsync: updateOrg } = useUpdateOrg();
|
||||||
|
|
||||||
|
const logout = useLogoutUser();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addOIDC",
|
"addOIDC",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
@@ -44,10 +47,34 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
}
|
||||||
text: `Failed to ${value ? "enable" : "disable"} OIDC SSO`,
|
};
|
||||||
type: "error"
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +129,24 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<h3 className="text-md text-mineshaft-100">Enforce OIDC SSO</h3>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Switch
|
||||||
|
id="enforce-org-auth"
|
||||||
|
isChecked={currentOrg?.authEnforced ?? false}
|
||||||
|
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Enforce members to authenticate via OIDC to access this organization
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<OIDCModal
|
<OIDCModal
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
handlePopUpClose={handlePopUpClose}
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
|||||||