Merge pull request #804 from Infisical/jumpcloud-saml
Optimize SAML SSO configuration flow and add documentation for Azure AD SAML
@@ -57,7 +57,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
} = req.body;
|
||||
|
||||
const plan = await EELicenseService.getPlan(organizationId);
|
||||
@@ -78,9 +77,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||
encryptedCert?: string;
|
||||
certIV?: string;
|
||||
certTag?: string;
|
||||
encryptedAudience?: string;
|
||||
audienceIV?: string;
|
||||
audienceTag?: string;
|
||||
}
|
||||
|
||||
const update: PatchUpdate = {};
|
||||
@@ -132,18 +128,6 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||
update.certIV = certIV;
|
||||
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(
|
||||
{
|
||||
@@ -207,8 +191,7 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
} = req.body;
|
||||
|
||||
const plan = await EELicenseService.getPlan(organizationId);
|
||||
@@ -238,12 +221,6 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
||||
iv: certIV,
|
||||
tag: certTag
|
||||
} = client.encryptSymmetric(cert, key);
|
||||
|
||||
const {
|
||||
ciphertext: encryptedAudience,
|
||||
iv: audienceIV,
|
||||
tag: audienceTag
|
||||
} = client.encryptSymmetric(audience, key);
|
||||
|
||||
const ssoConfig = await new SSOConfig({
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
@@ -257,10 +234,7 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
||||
issuerTag,
|
||||
encryptedCert,
|
||||
certIV,
|
||||
certTag,
|
||||
encryptedAudience,
|
||||
audienceIV,
|
||||
audienceTag
|
||||
certTag
|
||||
}).save();
|
||||
|
||||
return res.status(200).send(ssoConfig);
|
||||
|
||||
@@ -51,13 +51,6 @@ export const getSSOConfigHelper = async ({
|
||||
ssoConfig.certIV,
|
||||
ssoConfig.certTag
|
||||
);
|
||||
|
||||
const audience = client.decryptSymmetric(
|
||||
ssoConfig.encryptedAudience,
|
||||
key,
|
||||
ssoConfig.audienceIV,
|
||||
ssoConfig.audienceTag
|
||||
);
|
||||
|
||||
return ({
|
||||
_id: ssoConfig._id,
|
||||
@@ -66,7 +59,6 @@ export const getSSOConfigHelper = async ({
|
||||
isActive: ssoConfig.isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export enum AuthProvider {
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml"
|
||||
}
|
||||
|
||||
export interface ISSOConfig {
|
||||
organization: Types.ObjectId;
|
||||
authProvider: "okta-saml"
|
||||
authProvider: AuthProvider;
|
||||
isActive: boolean;
|
||||
encryptedEntryPoint: string;
|
||||
entryPointIV: string;
|
||||
@@ -13,9 +18,6 @@ export interface ISSOConfig {
|
||||
encryptedCert: string;
|
||||
certIV: string;
|
||||
certTag: string;
|
||||
encryptedAudience: string;
|
||||
audienceIV: string;
|
||||
audienceTag: string;
|
||||
}
|
||||
|
||||
const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||
@@ -26,9 +28,7 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: [
|
||||
"okta-saml"
|
||||
],
|
||||
enum: AuthProvider,
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
@@ -61,15 +61,6 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||
},
|
||||
certTag: {
|
||||
type: String
|
||||
},
|
||||
encryptedAudience: {
|
||||
type: String
|
||||
},
|
||||
audienceIV: {
|
||||
type: String
|
||||
},
|
||||
audienceTag: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import passport from "passport";
|
||||
import {
|
||||
AuthProvider
|
||||
} from "../../models";
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
@@ -87,12 +90,11 @@ router.post(
|
||||
locationOrganizationId: "body"
|
||||
}),
|
||||
body("organizationId").exists().trim(),
|
||||
body("authProvider").exists().isString(),
|
||||
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
|
||||
body("isActive").exists().isBoolean(),
|
||||
body("entryPoint").exists().isString(),
|
||||
body("issuer").exists().isString(),
|
||||
body("cert").exists().isString(),
|
||||
body("audience").exists().isString(),
|
||||
validateRequest,
|
||||
ssoController.createSSOConfig
|
||||
);
|
||||
@@ -113,7 +115,6 @@ router.patch(
|
||||
body("entryPoint").optional().isString(),
|
||||
body("issuer").optional().isString(),
|
||||
body("cert").optional().isString(),
|
||||
body("audience").optional().isString(),
|
||||
validateRequest,
|
||||
ssoController.updateSSOConfig
|
||||
);
|
||||
|
||||
@@ -135,24 +135,24 @@ const initializePassport = async () => {
|
||||
{
|
||||
passReqToCallback: true,
|
||||
getSamlOptions: async (req: any, done: any) => {
|
||||
const { ssoIdentifier } = req.params;
|
||||
|
||||
const ssoConfig = await getSSOConfigHelper({
|
||||
ssoConfigId: new Types.ObjectId(ssoIdentifier)
|
||||
});
|
||||
|
||||
const samlConfig = ({
|
||||
path: "/api/v1/auth/callback/saml",
|
||||
callbackURL: `${await getSiteURL()}/api/v1/auth/callback/saml`,
|
||||
entryPoint: ssoConfig.entryPoint,
|
||||
issuer: ssoConfig.issuer,
|
||||
cert: ssoConfig.cert,
|
||||
audience: ssoConfig.audience
|
||||
});
|
||||
|
||||
req.ssoConfig = ssoConfig;
|
||||
const { ssoIdentifier } = req.params;
|
||||
|
||||
const ssoConfig = await getSSOConfigHelper({
|
||||
ssoConfigId: new Types.ObjectId(ssoIdentifier)
|
||||
});
|
||||
|
||||
const samlConfig = ({
|
||||
path: `/api/v1/sso/saml2/${ssoIdentifier}`,
|
||||
callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`,
|
||||
entryPoint: ssoConfig.entryPoint,
|
||||
issuer: ssoConfig.issuer,
|
||||
cert: ssoConfig.cert,
|
||||
audience: await getSiteURL()
|
||||
});
|
||||
|
||||
req.ssoConfig = ssoConfig;
|
||||
|
||||
done(null, samlConfig);
|
||||
done(null, samlConfig);
|
||||
},
|
||||
},
|
||||
async (req: any, profile: any, done: any) => {
|
||||
@@ -161,7 +161,7 @@ const initializePassport = async () => {
|
||||
const organization = await Organization.findById(req.ssoConfig.organization);
|
||||
|
||||
if (!organization) return done(OrganizationNotFoundError());
|
||||
|
||||
|
||||
const email = profile.email;
|
||||
const firstName = profile.firstName;
|
||||
const lastName = profile.lastName;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
title: "SSO"
|
||||
description: "Log in to Infisical via SSO protocols"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Infisical currently only supports SAML SSO authentication with [Okta as the
|
||||
identity provider (IDP)](https://www.okta.com/). We're expanding support for
|
||||
other IDPs in the coming months, so stay tuned with this issue
|
||||
[here](https://github.com/Infisical/infisical/issues/442).
|
||||
</Warning>
|
||||
|
||||
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
<Note>
|
||||
Note that only members with the `owner` or `admin` roles in an organization
|
||||
can configure SSO for it.
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Okta SAML 2.0">
|
||||
1. In the Okta Admin Portal, select Applications > Applications from the
|
||||
navigation. On the Applications screen, select the Create App Integration
|
||||
button.
|
||||
|
||||

|
||||
|
||||
2. 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 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.
|
||||
- Audience URI (SP Entity ID): `https://app.infisical.com`
|
||||
|
||||

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

|
||||
|
||||
Once configured, select the Next button to proceed to the Feedback screen and select Finish.
|
||||
|
||||
5. 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:
|
||||
|
||||
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 -> Okta Audience URI (SP Entity ID)`
|
||||
- `Entrypoint -> Okta Identity Provider Single Sign-On URL`
|
||||
- `Issuer -> Identity Provider Issuer`
|
||||
- `Certificate -> X.509 Certificate`.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. Create the SSO configuration and copy your SSO identifier in Infisical; update `:identifier` from step 4 earlier to be this value.
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
|
||||
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
84
docs/documentation/platform/sso/azure.mdx
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: "Azure SAML"
|
||||
description: "Configure Azure SAML for Infisical SSO"
|
||||
---
|
||||
|
||||
1. In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
|
||||
Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application.
|
||||
|
||||

|
||||
|
||||
2. In the Azure Portal, navigate to the Azure Active Directory and select **Enterprise applications**. On this screen, select
|
||||
**+ New application**.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. On the next screen, press the **+ Create your own application** button.
|
||||
Give the application a unique, Infisical-specific name; choose the "Integrate any other application you don't find in the gallery (Non-gallery)"
|
||||
option and hit the **Create** button.
|
||||
|
||||

|
||||
|
||||
3. On the application overview screen, select **Single sign-on** from the left sidebar. From there,
|
||||
select the **SAML** single sign-on method.
|
||||
|
||||

|
||||
|
||||
4. Next, select **Edit** in the **Basic SAML Configuration** section and add/set the **Identifier (Entity ID)**
|
||||
to **Entity ID** and add/set the **Reply URL (Assertion Consumer Service URL)** to **ACS URL** from step 1.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
If you're self-hosting Infisical, then you will want to replace
|
||||
`https://app.infisical.com` with your own domain.
|
||||
</Note>
|
||||
|
||||
5. Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map:
|
||||
|
||||
- `email -> user.userprinciplename`
|
||||
- `firstName -> user.firstName`
|
||||
- `lastName -> user.lastName`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **SAML Certificates** section and set the **Signing Option** field to **Sign SAML response and assertion**.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
7. Get IdP values:
|
||||
|
||||
Back in the **Set up Single Sign-On with SAML** screen, copy the **Login URL**, **Azure AD Identifier** and **SAML Certificate** to use when finishing configuring Azure SAML in Infisical.
|
||||
|
||||
Back in Infisical, set **Login URL** and **Azure AD Identifier** from above. Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||

|
||||

|
||||
|
||||
<Note>
|
||||
When pasting the certificate into Infisical, you'll want to retain `-----BEGIN
|
||||
CERTIFICATE-----` and `-----END CERTIFICATE-----` at the first and last line
|
||||
of the text area respectively.
|
||||
|
||||
Having trouble?, try copying the X509 certificate information from the Federation Metadata XML file in Azure.
|
||||
|
||||
</Note>
|
||||
|
||||
7. Assignments
|
||||
|
||||
Finally, navigate to the **Users and groups** tab and select **+ Add user/group** to assign access to the login with SSO application on a user or group-level.
|
||||

|
||||
|
||||
8. 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 Azure.
|
||||
|
||||

|
||||
6
docs/documentation/platform/sso/jumpcloud.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "JumpCloud SAML"
|
||||
description: "Configure JumpCloud SAML for Infisical SSO"
|
||||
---
|
||||
|
||||
Coming soon.
|
||||
76
docs/documentation/platform/sso/okta.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: "Okta SAML"
|
||||
description: "Configure Okta SAML 2.0 for Infisical SSO"
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- 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 **Single sign-on URL** and **Audience URI (SP 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.
|
||||
|
||||

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

|
||||
|
||||
4. On the General Settings screen, give the application a unique name like Infisical and select **Next**.
|
||||
|
||||

|
||||
|
||||
5. On the Configure SAML screen, set the **Single sign-on URL** and **Audience URI (SP Entity ID)** from step 1.
|
||||
|
||||

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

|
||||
|
||||
Once configured, select **Next** to proceed to the Feedback screen and select **Finish**.
|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical.
|
||||
|
||||

|
||||
|
||||
Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**,
|
||||
and **Certificate** to **X.509 Certificate** from above. Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||

|
||||
|
||||
8. Finally, navigate to the **Assignments** tab and select **Assign**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||

|
||||
18
docs/documentation/platform/sso/overview.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "SSO Overview"
|
||||
description: "Log in to Infisical via SSO protocols"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Infisical currently only supports SAML SSO authentication with Okta and Azure
|
||||
AD. We're expanding support for other IdPs in the coming months, so stay tuned
|
||||
with this issue [here](https://github.com/Infisical/infisical/issues/442).
|
||||
</Warning>
|
||||
|
||||
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
|
||||
|
||||
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.
|
||||
|
||||
- [Okta SAML](/documentation/platform/sso/okta)
|
||||
- [Azure SAML](/documentation/platform/sso/azure)
|
||||
|
Before Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 563 KiB |
BIN
docs/images/sso/azure/assignment.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
docs/images/sso/azure/create-own-application.png
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
docs/images/sso/azure/edit-attributes-claims-2.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
docs/images/sso/azure/edit-attributes-claims.png
Normal file
|
After Width: | Height: | Size: 493 KiB |
BIN
docs/images/sso/azure/edit-basic-config-2.png
Normal file
|
After Width: | Height: | Size: 611 KiB |
BIN
docs/images/sso/azure/edit-basic-config.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
docs/images/sso/azure/edit-saml-certificate-2.png
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
docs/images/sso/azure/edit-saml-certificate.png
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
docs/images/sso/azure/enable-saml.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
docs/images/sso/azure/enterprise-applications.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
docs/images/sso/azure/idp-values-2.png
Normal file
|
After Width: | Height: | Size: 521 KiB |
BIN
docs/images/sso/azure/idp-values.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
docs/images/sso/azure/init-config.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
docs/images/sso/azure/new-application.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
docs/images/sso/azure/sso-method.png
Normal file
|
After Width: | Height: | Size: 484 KiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 316 KiB |
BIN
docs/images/sso/okta/configure-saml.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 381 KiB |
BIN
docs/images/sso/okta/enable-saml.png
Normal file
|
After Width: | Height: | Size: 521 KiB |
BIN
docs/images/sso/okta/general-settings.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/images/sso/okta/idp-values-2.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
|
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
BIN
docs/images/sso/okta/init-config.png
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
docs/images/sso/okta/view-setup-instructions.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -121,7 +121,15 @@
|
||||
"documentation/platform/token",
|
||||
"documentation/platform/ip-allowlisting",
|
||||
"documentation/platform/mfa",
|
||||
"documentation/platform/saml"
|
||||
{
|
||||
"group": "SSO",
|
||||
"pages": [
|
||||
"documentation/platform/sso/overview",
|
||||
"documentation/platform/sso/okta",
|
||||
"documentation/platform/sso/azure",
|
||||
"documentation/platform/sso/jumpcloud"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,8 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
getAuthToken,
|
||||
getMfaTempToken,
|
||||
getSignupTempToken} from "@app/reactQuery";
|
||||
getSignupTempToken
|
||||
} from "@app/reactQuery";
|
||||
|
||||
export const apiRequest = axios.create({
|
||||
baseURL: "/",
|
||||
|
||||
@@ -29,8 +29,7 @@ export const useCreateSSOConfig = () => {
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
}: {
|
||||
organizationId: string;
|
||||
authProvider: string;
|
||||
@@ -38,7 +37,6 @@ export const useCreateSSOConfig = () => {
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
audience: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/sso/config",
|
||||
@@ -48,8 +46,7 @@ export const useCreateSSOConfig = () => {
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
}
|
||||
);
|
||||
|
||||
@@ -70,8 +67,7 @@ export const useUpdateSSOConfig = () => {
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
}: {
|
||||
organizationId: string;
|
||||
authProvider?: string;
|
||||
@@ -79,7 +75,6 @@ export const useUpdateSSOConfig = () => {
|
||||
entryPoint?: string;
|
||||
issuer?: string;
|
||||
cert?: string;
|
||||
audience?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch(
|
||||
"/api/v1/sso/config",
|
||||
@@ -89,8 +84,7 @@ export const useUpdateSSOConfig = () => {
|
||||
...(isActive !== undefined ? { isActive } : {}),
|
||||
...(entryPoint !== undefined ? { entryPoint } : {}),
|
||||
...(issuer !== undefined ? { issuer } : {}),
|
||||
...(cert !== undefined ? { cert } : {}),
|
||||
...(audience !== undefined ? { audience } : {})
|
||||
...(cert !== undefined ? { cert } : {})
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
||||
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useCreateSSOConfig,
|
||||
useGetSSOConfig,
|
||||
useUpdateSSOConfig
|
||||
} from "@app/hooks/api";
|
||||
@@ -13,7 +14,8 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
const ssoAuthProviderMap: { [key: string]: string } = {
|
||||
"okta-saml": "Okta SAML 2.0"
|
||||
"okta-saml": "Okta SAML",
|
||||
"azure-saml": "Azure SAML"
|
||||
}
|
||||
|
||||
export const OrgSSOSection = (): JSX.Element => {
|
||||
@@ -27,6 +29,8 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
|
||||
|
||||
const handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
@@ -49,6 +53,31 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
@@ -57,13 +86,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
</h2>
|
||||
{!isLoading && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription?.samlSSO) {
|
||||
handlePopUpOpen("addSSO");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
onClick={addSSOBtnClick}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
@@ -71,39 +94,33 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!isLoading && data && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data.isActive}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
</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>
|
||||
</>
|
||||
{data && (
|
||||
<div className="mb-4">
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
</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
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
|
||||
@@ -21,16 +21,21 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
enum AuthProvider {
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml"
|
||||
}
|
||||
|
||||
const ssoAuthProviders = [
|
||||
{ label: "Okta SAML 2.0", value: "okta-saml" }
|
||||
{ label: "Okta SAML", value: AuthProvider.OKTA_SAML },
|
||||
{ label: "Azure SAML", value: AuthProvider.AZURE_SAML }
|
||||
];
|
||||
|
||||
const schema = yup.object({
|
||||
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"),
|
||||
cert: yup.string().required("IDP's public signing certificate is required"),
|
||||
audience: yup.string().required("Expected SAML response audience is required"),
|
||||
cert: yup.string().required("IdP's public signing certificate is required")
|
||||
}).required();
|
||||
|
||||
export type AddSSOFormData = yup.InferType<typeof schema>;
|
||||
@@ -59,7 +64,7 @@ export const SSOModal = ({
|
||||
watch,
|
||||
} = useForm<AddSSOFormData>({
|
||||
defaultValues: {
|
||||
authProvider: "okta-saml"
|
||||
authProvider: AuthProvider.OKTA_SAML
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
@@ -70,8 +75,7 @@ export const SSOModal = ({
|
||||
authProvider: data?.authProvider ?? "",
|
||||
entryPoint: data?.entryPoint ?? "",
|
||||
issuer: data?.issuer ?? "",
|
||||
cert: data?.cert ?? "",
|
||||
audience: data?.audience ?? ""
|
||||
cert: data?.cert ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -80,8 +84,7 @@ export const SSOModal = ({
|
||||
authProvider,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
}: AddSSOFormData) => {
|
||||
try {
|
||||
if (!currentOrg) return;
|
||||
@@ -93,8 +96,7 @@ export const SSOModal = ({
|
||||
isActive: false,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
});
|
||||
} else {
|
||||
await updateMutateAsync({
|
||||
@@ -103,8 +105,7 @@ export const SSOModal = ({
|
||||
isActive: false,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
cert
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,6 +124,38 @@ export const SSOModal = ({
|
||||
}
|
||||
}
|
||||
|
||||
const renderLabels = (authProvider: string) => {
|
||||
switch (authProvider){
|
||||
case AuthProvider.OKTA_SAML:
|
||||
return ({
|
||||
acsUrl: "Single sign-on URL",
|
||||
entityId: "Audience URI (SP Entity ID)",
|
||||
entryPoint: "Identity Provider Single Sign-On URL",
|
||||
entryPointPlaceholder: "https://your-domain.okta.com/app/app-name/xxx/sso/saml",
|
||||
issuer: "Identity Provider Issuer",
|
||||
issuerPlaceholder: "http://www.okta.com/xxx"
|
||||
});
|
||||
case AuthProvider.AZURE_SAML:
|
||||
return ({
|
||||
acsUrl: "Reply URL (Assertion Consumer Service URL)",
|
||||
entityId: "Identifier (Entity ID)",
|
||||
entryPoint: "Login URL",
|
||||
entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2",
|
||||
issuer: "Azure AD Identifier",
|
||||
issuerPlaceholder: "https://sts.windows.net/xxx/"
|
||||
});
|
||||
default:
|
||||
return ({
|
||||
acsUrl: "ACS URL",
|
||||
entityId: "Entity ID",
|
||||
entryPoint: "Entrypoint",
|
||||
entryPointPlaceholder: "Enter entrypoint...",
|
||||
issuer: "Issuer",
|
||||
issuerPlaceholder: "Enter placeholder..."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const authProvider = watch("authProvider");
|
||||
|
||||
return (
|
||||
@@ -160,36 +193,28 @@ export const SSOModal = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{authProvider && authProvider === "okta-saml" && (
|
||||
{authProvider && data && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="audience"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audience"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://your-domain.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).acsUrl}</h3>
|
||||
<p className="text-gray-400 text-md break-all">{`${window.origin}/api/v1/sso/saml2/${data._id}`}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).entityId}</h3>
|
||||
<p className="text-gray-400 text-md">{window.origin}</p>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="entryPoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Entrypoint"
|
||||
label={renderLabels(authProvider).entryPoint}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://your-domain.okta.com/app/app-name/xxx/sso/saml"
|
||||
placeholder={renderLabels(authProvider).entryPointPlaceholder}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -199,13 +224,13 @@ export const SSOModal = ({
|
||||
name="issuer"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuer"
|
||||
label={renderLabels(authProvider).issuer}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="http://www.okta.com/xxx"
|
||||
placeholder={renderLabels(authProvider).issuerPlaceholder}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||