Revise SAML flow, update Okta SAML docs
@@ -57,7 +57,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert,
|
||||||
audience
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(organizationId);
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
@@ -78,9 +77,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
encryptedCert?: string;
|
encryptedCert?: string;
|
||||||
certIV?: string;
|
certIV?: string;
|
||||||
certTag?: string;
|
certTag?: string;
|
||||||
encryptedAudience?: string;
|
|
||||||
audienceIV?: string;
|
|
||||||
audienceTag?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: PatchUpdate = {};
|
const update: PatchUpdate = {};
|
||||||
@@ -133,18 +129,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
update.certTag = certTag;
|
update.certTag = certTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audience) {
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedAudience,
|
|
||||||
iv: audienceIV,
|
|
||||||
tag: audienceTag
|
|
||||||
} = client.encryptSymmetric(audience, key);
|
|
||||||
|
|
||||||
update.encryptedAudience = encryptedAudience;
|
|
||||||
update.audienceIV = audienceIV;
|
|
||||||
update.audienceTag = audienceTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ssoConfig = await SSOConfig.findOneAndUpdate(
|
const ssoConfig = await SSOConfig.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
organization: new Types.ObjectId(organizationId)
|
organization: new Types.ObjectId(organizationId)
|
||||||
@@ -207,8 +191,7 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
|||||||
isActive,
|
isActive,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(organizationId);
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
@@ -239,12 +222,6 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
|||||||
tag: certTag
|
tag: certTag
|
||||||
} = client.encryptSymmetric(cert, key);
|
} = client.encryptSymmetric(cert, key);
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedAudience,
|
|
||||||
iv: audienceIV,
|
|
||||||
tag: audienceTag
|
|
||||||
} = client.encryptSymmetric(audience, key);
|
|
||||||
|
|
||||||
const ssoConfig = await new SSOConfig({
|
const ssoConfig = await new SSOConfig({
|
||||||
organization: new Types.ObjectId(organizationId),
|
organization: new Types.ObjectId(organizationId),
|
||||||
authProvider,
|
authProvider,
|
||||||
@@ -257,10 +234,7 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
|||||||
issuerTag,
|
issuerTag,
|
||||||
encryptedCert,
|
encryptedCert,
|
||||||
certIV,
|
certIV,
|
||||||
certTag,
|
certTag
|
||||||
encryptedAudience,
|
|
||||||
audienceIV,
|
|
||||||
audienceTag
|
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
return res.status(200).send(ssoConfig);
|
return res.status(200).send(ssoConfig);
|
||||||
|
|||||||
@@ -52,13 +52,6 @@ export const getSSOConfigHelper = async ({
|
|||||||
ssoConfig.certTag
|
ssoConfig.certTag
|
||||||
);
|
);
|
||||||
|
|
||||||
const audience = client.decryptSymmetric(
|
|
||||||
ssoConfig.encryptedAudience,
|
|
||||||
key,
|
|
||||||
ssoConfig.audienceIV,
|
|
||||||
ssoConfig.audienceTag
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
_id: ssoConfig._id,
|
_id: ssoConfig._id,
|
||||||
organization: ssoConfig.organization,
|
organization: ssoConfig.organization,
|
||||||
@@ -66,7 +59,6 @@ export const getSSOConfigHelper = async ({
|
|||||||
isActive: ssoConfig.isActive,
|
isActive: ssoConfig.isActive,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
|
export enum AuthProvider {
|
||||||
|
OKTA_SAML = "okta-saml"
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISSOConfig {
|
export interface ISSOConfig {
|
||||||
organization: Types.ObjectId;
|
organization: Types.ObjectId;
|
||||||
authProvider: "okta-saml"
|
authProvider: AuthProvider;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
encryptedEntryPoint: string;
|
encryptedEntryPoint: string;
|
||||||
entryPointIV: string;
|
entryPointIV: string;
|
||||||
@@ -13,9 +17,6 @@ export interface ISSOConfig {
|
|||||||
encryptedCert: string;
|
encryptedCert: string;
|
||||||
certIV: string;
|
certIV: string;
|
||||||
certTag: string;
|
certTag: string;
|
||||||
encryptedAudience: string;
|
|
||||||
audienceIV: string;
|
|
||||||
audienceTag: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssoConfigSchema = new Schema<ISSOConfig>(
|
const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||||
@@ -26,9 +27,7 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
|
|||||||
},
|
},
|
||||||
authProvider: {
|
authProvider: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [
|
enum: AuthProvider,
|
||||||
"okta-saml"
|
|
||||||
],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
@@ -61,15 +60,6 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
|
|||||||
},
|
},
|
||||||
certTag: {
|
certTag: {
|
||||||
type: String
|
type: String
|
||||||
},
|
|
||||||
encryptedAudience: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
audienceIV: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
audienceTag: {
|
|
||||||
type: String
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
|
import {
|
||||||
|
AuthProvider
|
||||||
|
} from "../../models";
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireOrganizationAuth,
|
requireOrganizationAuth,
|
||||||
@@ -87,12 +90,11 @@ router.post(
|
|||||||
locationOrganizationId: "body"
|
locationOrganizationId: "body"
|
||||||
}),
|
}),
|
||||||
body("organizationId").exists().trim(),
|
body("organizationId").exists().trim(),
|
||||||
body("authProvider").exists().isString(),
|
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
|
||||||
body("isActive").exists().isBoolean(),
|
body("isActive").exists().isBoolean(),
|
||||||
body("entryPoint").exists().isString(),
|
body("entryPoint").exists().isString(),
|
||||||
body("issuer").exists().isString(),
|
body("issuer").exists().isString(),
|
||||||
body("cert").exists().isString(),
|
body("cert").exists().isString(),
|
||||||
body("audience").exists().isString(),
|
|
||||||
validateRequest,
|
validateRequest,
|
||||||
ssoController.createSSOConfig
|
ssoController.createSSOConfig
|
||||||
);
|
);
|
||||||
@@ -113,7 +115,6 @@ router.patch(
|
|||||||
body("entryPoint").optional().isString(),
|
body("entryPoint").optional().isString(),
|
||||||
body("issuer").optional().isString(),
|
body("issuer").optional().isString(),
|
||||||
body("cert").optional().isString(),
|
body("cert").optional().isString(),
|
||||||
body("audience").optional().isString(),
|
|
||||||
validateRequest,
|
validateRequest,
|
||||||
ssoController.updateSSOConfig
|
ssoController.updateSSOConfig
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class EELicenseService {
|
|||||||
customRateLimits: true,
|
customRateLimits: true,
|
||||||
customAlerts: true,
|
customAlerts: true,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true
|
has_used_trial: true
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const initializePassport = async () => {
|
|||||||
entryPoint: ssoConfig.entryPoint,
|
entryPoint: ssoConfig.entryPoint,
|
||||||
issuer: ssoConfig.issuer,
|
issuer: ssoConfig.issuer,
|
||||||
cert: ssoConfig.cert,
|
cert: ssoConfig.cert,
|
||||||
audience: ssoConfig.audience
|
audience: await getSiteURL()
|
||||||
});
|
});
|
||||||
|
|
||||||
req.ssoConfig = ssoConfig;
|
req.ssoConfig = ssoConfig;
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ title: "Azure SAML"
|
|||||||
description: "Configure Azure SAML for Infisical SSO"
|
description: "Configure Azure SAML for Infisical SSO"
|
||||||
---
|
---
|
||||||
|
|
||||||
1. In the Azure Portal, navigate to the Azure Active Directory and select Enterprise applications. On this screen, select
|
1. In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
|
||||||
|
Next, copy the **ACS URL** and **Entity ID** to use when configuring the Okta SAML 2.0 application.
|
||||||
|
|
||||||
|
2. In the Azure Portal, navigate to the Azure Active Directory and select Enterprise applications. On this screen, select
|
||||||
the **+ New application** button.
|
the **+ New application** button.
|
||||||
|
|
||||||
TODO: insert image.
|
TODO: insert image.
|
||||||
|
|||||||
@@ -1,72 +1,77 @@
|
|||||||
---
|
---
|
||||||
title: "Okta SAML"
|
title: "Okta SAML"
|
||||||
description: "Configure Okta SAML for Infisical SSO"
|
description: "Configure Okta SAML 2.0 for Infisical SSO"
|
||||||
---
|
---
|
||||||
|
|
||||||
1. In the Okta Admin Portal, select Applications > Applications from the
|
Prerequisites:
|
||||||
navigation. On the Applications screen, select the Create App Integration
|
- Okta Developer Account with access to create custom application integrations.
|
||||||
|
|
||||||
|
|
||||||
|
1. In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
|
||||||
|
Next, copy the **ACS URL** and **Entity ID** to use when configuring the Okta SAML 2.0 application.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. In the Okta Admin Portal, select Applications > Applications from the
|
||||||
|
navigation. On the Applications screen, select the **Create App Integration**
|
||||||
button.
|
button.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
2. In the Create a New Application Integration dialog, select the SAML 2.0 radio button:
|
3. In the Create a New Application Integration dialog, select the **SAML 2.0** radio button:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
3. On the General Settings screen, give the application a unique, Infisical-specific name and select Next.
|
4. On the General Settings screen, give the application a unique name like Infisical and select **Next**.
|
||||||
|
|
||||||
4. On the Configure SAML screen, configure the following fields:
|

|
||||||
|
|
||||||
- Single sign on URL: `https://app.infisical.com/api/v1/sso/saml2/:identifier`; we'll update the `:identifier` part later in step 6.
|
5. On the Configure SAML screen, set the **Single sign-on URL** to **ACS URL** and **Audience URI (SP Entity ID)** to
|
||||||
- Audience URI (SP Entity ID): `https://app.infisical.com`
|
**Entity ID** from step 1.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
If you're self-hosting Infisical, then you will want to replace
|
If you're self-hosting Infisical, then you will want to replace
|
||||||
`https://app.infisical.com` with your own domain.
|
`https://app.infisical.com` with your own domain.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
4. Also on the Configure SAML screen, configure the Attribute Statements to map:
|
6. Also on the Configure SAML screen, configure the **Attribute Statements** to map:
|
||||||
|
|
||||||
- `id -> user.id`,
|
- `id -> user.id`,
|
||||||
- `email -> user.email`,
|
- `email -> user.email`,
|
||||||
- `firstName -> user.firstName`
|
- `firstName -> user.firstName`
|
||||||
- `lastName -> user.lastName`
|
- `lastName -> user.lastName`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Once configured, select the Next button to proceed to the Feedback screen and select Finish.
|
Once configured, select the **Next** button to proceed to the Feedback screen and select **Finish**.
|
||||||
|
|
||||||
5. Get IdP values
|
7. Get IdP values
|
||||||
|
|
||||||
Once your application is created, select the Sign On tab for the app and select the View Setup Instructions button located on the right side of the screen:
|
Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen:
|
||||||
|
|
||||||
Copy the Identity Provider Single Sign-On URL, the Identity Provider Issuer, and the X.509 Certificate to be pasted into your Infisical SAML SSO configuration details with the following map:
|

|
||||||
|
|
||||||
- `Audience -> Audience URI (SP Entity ID) from Okta`
|
Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring the Okta SAML in Infisical.
|
||||||
- `Entrypoint -> Identity Provider Single Sign-On URL from Okta`
|
|
||||||
- `Issuer -> Identity Provider Issuer from Okta`
|
|
||||||
- `Certificate -> X.509 Certificate from Okta`
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
Back in Infisical, set **Entrypoint** to **Identity Provider Single Sign-On URL**, **Issuer** to **Identity Provider Issuer**,
|
||||||
|
and **Certificate** to **X.509 Certificate** from above. Once you've done that, press **Add** to complete the required configuration.
|
||||||
|
|
||||||
6. Create the SSO configuration and copy your SSO identifier in Infisical; update `:identifier` from step 4 earlier to be this value.
|

|
||||||
|
|
||||||

|
8. Finally, navigate to the **Assignments** tab and select **Assign**
|
||||||
|
|
||||||
7. Assignments
|
|
||||||
|
|
||||||
Finally, navigate to the Assignments tab and select the Assign button:
|
|
||||||
|
|
||||||
You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
|
You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
At this point, you have configured everything you need within the context of the Okta Admin Portal.
|
At this point, you have configured everything you need within the context of the Okta Admin Portal.
|
||||||
|
|
||||||
8. Return to Infisical and enable SAML SSO.
|
9. Return to Infisical and enable SAML SSO.
|
||||||
|
|
||||||
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
|
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
|
||||||
|
|
||||||
|

|
||||||
@@ -14,10 +14,3 @@ You can configure your organization in Infisical to have members authenticate wi
|
|||||||
|
|
||||||
To note, configuring SSO retains the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps. In all login with SSO implementations,
|
To note, configuring SSO retains the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps. In all login with SSO implementations,
|
||||||
your IdP cannot and will not have access to the decryption key needed to decrypt your secrets.
|
your IdP cannot and will not have access to the decryption key needed to decrypt your secrets.
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Head over to your organization Settings > Authentication > SAML SSO Configuration.
|
|
||||||
|
|
||||||
Next, press "Set up SAML SSO" in the SAML SSO and follow the instructions
|
|
||||||
below to configure SSO for your identity provider.
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 563 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 563 KiB After Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 386 KiB |
@@ -4,7 +4,8 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
|||||||
import {
|
import {
|
||||||
getAuthToken,
|
getAuthToken,
|
||||||
getMfaTempToken,
|
getMfaTempToken,
|
||||||
getSignupTempToken} from "@app/reactQuery";
|
getSignupTempToken
|
||||||
|
} from "@app/reactQuery";
|
||||||
|
|
||||||
export const apiRequest = axios.create({
|
export const apiRequest = axios.create({
|
||||||
baseURL: "/",
|
baseURL: "/",
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ export const useCreateSSOConfig = () => {
|
|||||||
isActive,
|
isActive,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
}: {
|
}: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
authProvider: string;
|
authProvider: string;
|
||||||
@@ -38,7 +37,6 @@ export const useCreateSSOConfig = () => {
|
|||||||
entryPoint: string;
|
entryPoint: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
cert: string;
|
cert: string;
|
||||||
audience: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post(
|
const { data } = await apiRequest.post(
|
||||||
"/api/v1/sso/config",
|
"/api/v1/sso/config",
|
||||||
@@ -48,8 +46,7 @@ export const useCreateSSOConfig = () => {
|
|||||||
isActive,
|
isActive,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,8 +67,7 @@ export const useUpdateSSOConfig = () => {
|
|||||||
isActive,
|
isActive,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
}: {
|
}: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
authProvider?: string;
|
authProvider?: string;
|
||||||
@@ -79,7 +75,6 @@ export const useUpdateSSOConfig = () => {
|
|||||||
entryPoint?: string;
|
entryPoint?: string;
|
||||||
issuer?: string;
|
issuer?: string;
|
||||||
cert?: string;
|
cert?: string;
|
||||||
audience?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch(
|
const { data } = await apiRequest.patch(
|
||||||
"/api/v1/sso/config",
|
"/api/v1/sso/config",
|
||||||
@@ -89,8 +84,7 @@ export const useUpdateSSOConfig = () => {
|
|||||||
...(isActive !== undefined ? { isActive } : {}),
|
...(isActive !== undefined ? { isActive } : {}),
|
||||||
...(entryPoint !== undefined ? { entryPoint } : {}),
|
...(entryPoint !== undefined ? { entryPoint } : {}),
|
||||||
...(issuer !== undefined ? { issuer } : {}),
|
...(issuer !== undefined ? { issuer } : {}),
|
||||||
...(cert !== undefined ? { cert } : {}),
|
...(cert !== undefined ? { cert } : {})
|
||||||
...(audience !== undefined ? { audience } : {})
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
|||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetSSOConfig,
|
useGetSSOConfig,
|
||||||
|
useCreateSSOConfig,
|
||||||
useUpdateSSOConfig
|
useUpdateSSOConfig
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
@@ -27,6 +28,8 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
"addSSO"
|
"addSSO"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
|
||||||
|
|
||||||
const handleSamlSSOToggle = async (value: boolean) => {
|
const handleSamlSSOToggle = async (value: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
@@ -49,6 +52,33 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addSSOBtnClick = async () => {
|
||||||
|
try {
|
||||||
|
if (subscription?.samlSSO && currentOrg) {
|
||||||
|
if (!data) {
|
||||||
|
// case: SAML SSO is not configured
|
||||||
|
// -> initialize empty SAML SSO configuration
|
||||||
|
await createMutateAsync({
|
||||||
|
organizationId: currentOrg._id,
|
||||||
|
authProvider: "okta-saml",
|
||||||
|
isActive: false,
|
||||||
|
entryPoint: "",
|
||||||
|
issuer: "",
|
||||||
|
cert: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("addSSO");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("getSSOConfig: ", data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
@@ -57,13 +87,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
</h2>
|
</h2>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={addSSOBtnClick}
|
||||||
if (subscription?.samlSSO) {
|
|
||||||
handlePopUpOpen("addSSO");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
>
|
>
|
||||||
@@ -71,39 +95,33 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isLoading && data && (
|
{data && (
|
||||||
<>
|
<div className="mb-4">
|
||||||
<div className="mb-4">
|
<Switch
|
||||||
<Switch
|
id="enable-saml-sso"
|
||||||
id="enable-saml-sso"
|
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
isChecked={data ? data.isActive : false}
|
||||||
isChecked={data.isActive}
|
>
|
||||||
>
|
Enable SAML SSO
|
||||||
Enable SAML SSO
|
</Switch>
|
||||||
</Switch>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
|
|
||||||
<p className="text-gray-400 text-md">{data._id}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-mineshaft-400 text-sm">Type</h3>
|
|
||||||
<p className="text-gray-400 text-md">{ssoAuthProviderMap[data.authProvider]}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-mineshaft-400 text-sm">Audience</h3>
|
|
||||||
<p className="text-gray-400 text-md">{data.audience}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
|
|
||||||
<p className="text-gray-400 text-md">{data.entryPoint}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
|
|
||||||
<p className="text-gray-400 text-md">{data.issuer}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
|
||||||
|
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Type</h3>
|
||||||
|
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
|
||||||
|
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
|
||||||
|
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p>
|
||||||
|
</div>
|
||||||
<SSOModal
|
<SSOModal
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
handlePopUpClose={handlePopUpClose}
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -29,8 +28,7 @@ const schema = yup.object({
|
|||||||
authProvider: yup.string().required("SSO Type is required"),
|
authProvider: yup.string().required("SSO Type is required"),
|
||||||
entryPoint: yup.string().required("IDP entrypoint is required"),
|
entryPoint: yup.string().required("IDP entrypoint is required"),
|
||||||
issuer: yup.string().required("Issuer string is required"),
|
issuer: yup.string().required("Issuer string is required"),
|
||||||
cert: yup.string().required("IDP's public signing certificate is required"),
|
cert: yup.string().required("IDP's public signing certificate is required")
|
||||||
audience: yup.string().required("Expected SAML response audience is required"),
|
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export type AddSSOFormData = yup.InferType<typeof schema>;
|
export type AddSSOFormData = yup.InferType<typeof schema>;
|
||||||
@@ -70,8 +68,7 @@ export const SSOModal = ({
|
|||||||
authProvider: data?.authProvider ?? "",
|
authProvider: data?.authProvider ?? "",
|
||||||
entryPoint: data?.entryPoint ?? "",
|
entryPoint: data?.entryPoint ?? "",
|
||||||
issuer: data?.issuer ?? "",
|
issuer: data?.issuer ?? "",
|
||||||
cert: data?.cert ?? "",
|
cert: data?.cert ?? ""
|
||||||
audience: data?.audience ?? ""
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -80,8 +77,7 @@ export const SSOModal = ({
|
|||||||
authProvider,
|
authProvider,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
}: AddSSOFormData) => {
|
}: AddSSOFormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
@@ -93,8 +89,7 @@ export const SSOModal = ({
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@@ -103,8 +98,7 @@ export const SSOModal = ({
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
issuer,
|
issuer,
|
||||||
cert,
|
cert
|
||||||
audience
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,24 +154,16 @@ export const SSOModal = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{authProvider && authProvider === "okta-saml" && (
|
{authProvider && authProvider === "okta-saml" && data && (
|
||||||
<>
|
<>
|
||||||
<Controller
|
<div className="mb-4">
|
||||||
control={control}
|
<h3 className="text-mineshaft-400 text-sm">ACS URL</h3>
|
||||||
name="audience"
|
<p className="text-gray-400 text-md break-all">{`${window.origin}/api/v1/sso/saml2/${data._id}`}</p>
|
||||||
render={({ field, fieldState: { error } }) => (
|
</div>
|
||||||
<FormControl
|
<div className="mb-4">
|
||||||
label="Audience"
|
<h3 className="text-mineshaft-400 text-sm">Entity ID</h3>
|
||||||
errorText={error?.message}
|
<p className="text-gray-400 text-md">{window.origin}</p>
|
||||||
isError={Boolean(error)}
|
</div>
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="https://your-domain.com"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="entryPoint"
|
name="entryPoint"
|
||||||
|
|||||||