mirror of https://github.com/boxyhq/jackson.git
Renaming routes for Identity Federation (#2618)
* WIP
* updated swagger file
* renamed routes
* renamed test folder
* separate section for Identity Federation
* sso-tracer -> sso-traces
* don't change ACS url for SAML federation
* SAMLFederation -> IdentityFederation
* SAMLFederation -> IdentityFederation
* keep api/federated-saml but move to api/identity-federation
* test old route as well
* fixed test
* fixed test
* retry tests 3 times
* updated deployment
* WIP create SAML Fed app
(cherry picked from commit 3d15b20a2d
)
* Add Admin Portal SSO via SAML Fed
* Minor tweaks
* Use fixture and login using federated connection
* Cleanup SAML fed connection after test, disable failing assertion for now
* Remove only
* Use MockSAML endpoint from env
* Cleanup SSO connections mapped to SAML Fed
* OIDC Fed spec
* Try with higher timeout
* Mutate on page load
* Put back assertion
* Remove assertion and mutate for now
* SAML Fed App + 2 SAML Providers
* Take in optional tenant/product for fixture method
* SAML Fed + 2 OIDC providers
* SAML Fed test cases for single provider
* Tweak title
* Replace swr with fetch
* Remove only
* Bump up timeout to 100s
* Add more test cases for OIDC Fed
* Refactor fetch with hooks
* locale tweaks
* Also try with the other provider
* Fixture support SAML add via raw metadata
* Add second SAML connection using raw metadata
* Revert "Add second SAML connection using raw metadata"
* Revert "Fixture support SAML add via raw metadata"
---------
Co-authored-by: ukrocks007 <ukrocks.mehta@gmail.com>
Co-authored-by: Aswin V <vaswin91@gmail.com>
This commit is contained in:
parent
147fbaa9ec
commit
6bfb89a74e
|
@ -40,7 +40,7 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
href: '/admin/sso-connection',
|
||||
text: t('enterprise_sso'),
|
||||
icon: SSOLogo,
|
||||
active: asPath.includes('/admin/sso-connection') || asPath.includes('/admin/federated-saml'),
|
||||
active: asPath.includes('/admin/sso-connection'),
|
||||
items: [
|
||||
{
|
||||
href: '/admin/sso-connection',
|
||||
|
@ -54,14 +54,22 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
active: asPath.includes('/admin/sso-connection/setup-link'),
|
||||
},
|
||||
{
|
||||
href: '/admin/federated-saml',
|
||||
text: t('saml_federation'),
|
||||
active: asPath.includes('/admin/federated-saml'),
|
||||
},
|
||||
{
|
||||
href: '/admin/sso-tracer',
|
||||
href: '/admin/sso-traces',
|
||||
text: t('bui-tracer-title'),
|
||||
active: asPath.includes('/admin/sso-tracer'),
|
||||
active: asPath.includes('/admin/sso-traces'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/admin/identity-federation',
|
||||
text: t('identity_federation'),
|
||||
icon: SSOLogo,
|
||||
active: asPath.includes('/admin/identity-federation'),
|
||||
items: [
|
||||
{
|
||||
href: '/admin/identity-federation',
|
||||
text: t('apps'),
|
||||
active: asPath.includes('/admin/identity-federation'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { SAMLFederationApp } from '@boxyhq/saml-jackson';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use(options);
|
||||
|
||||
const expectedApp = {
|
||||
name: 'Test App',
|
||||
tenant: 'api-boxyhq',
|
||||
product: 'api-saml-jackson',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id',
|
||||
acsUrl: 'https://boxyhq.com/acs',
|
||||
};
|
||||
|
||||
let app = {} as SAMLFederationApp;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const response = await request.post('/api/v1/federated-saml', {
|
||||
data: {
|
||||
...expectedApp,
|
||||
},
|
||||
});
|
||||
|
||||
app = (await response.json()).data;
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(201);
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/federated-saml', () => {
|
||||
test('Fetch app by id', async ({ request }) => {
|
||||
const response = await request.get(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(app);
|
||||
});
|
||||
|
||||
test('Fetch app by tenant and product', async ({ request }) => {
|
||||
const response = await request.get(
|
||||
`/api/v1/federated-saml?tenant=${app?.tenant}&product=${app?.product}`
|
||||
);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(app);
|
||||
});
|
||||
|
||||
test('Fetch app by product', async ({ request }) => {
|
||||
const response = await request.get(`/api/v1/federated-saml/product?product=${app?.product}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject([app]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PATCH /api/v1/federated-saml', () => {
|
||||
test('Update app by id', async ({ request }) => {
|
||||
const response = await request.patch(`/api/v1/federated-saml`, {
|
||||
data: {
|
||||
id: app?.id,
|
||||
name: 'Updated App',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...app,
|
||||
name: 'Updated App',
|
||||
});
|
||||
});
|
||||
|
||||
test('Update app by tenant and product', async ({ request }) => {
|
||||
const response = await request.patch(`/api/v1/federated-saml`, {
|
||||
data: {
|
||||
id: app?.id,
|
||||
name: 'Updated App 2',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...app,
|
||||
name: 'Updated App 2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DELETE /api/v1/federated-saml', () => {
|
||||
test('Delete app by id', async ({ request }) => {
|
||||
const response = await request.delete(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({});
|
||||
|
||||
// Confirm app is deleted
|
||||
const response2 = await request.get(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
expect(response2.ok()).toBe(false);
|
||||
expect(response2.status()).toBe(404);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,175 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { IdentityFederationApp } from '@boxyhq/saml-jackson';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use(options);
|
||||
|
||||
const expectedApp = {
|
||||
name: 'Test App',
|
||||
tenant: 'api-boxyhq',
|
||||
product: 'api-saml-jackson',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id',
|
||||
acsUrl: 'https://boxyhq.com/acs',
|
||||
};
|
||||
const expectedApp1 = {
|
||||
name: 'Test App1',
|
||||
tenant: 'api-boxyhq-1',
|
||||
product: 'api-saml-jackson-1',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id-1',
|
||||
acsUrl: 'https://boxyhq.com/acs-1',
|
||||
};
|
||||
const newUrlPath = 'identity-federation';
|
||||
const oldUrlPath = 'federated-saml';
|
||||
|
||||
let app = {} as IdentityFederationApp;
|
||||
let app1 = {} as IdentityFederationApp;
|
||||
|
||||
const beforeAll = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? expectedApp : expectedApp1;
|
||||
const response = await request.post(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
...currApp,
|
||||
},
|
||||
});
|
||||
|
||||
const localApp = (await response.json()).data;
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(201);
|
||||
|
||||
return localApp;
|
||||
};
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
app = await beforeAll(oldUrlPath, request);
|
||||
app1 = await beforeAll(newUrlPath, request);
|
||||
});
|
||||
|
||||
const testGETById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(currApp);
|
||||
};
|
||||
|
||||
const testGETByTenantProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(
|
||||
`/api/v1/${urlPath}?tenant=${currApp?.tenant}&product=${currApp?.product}`
|
||||
);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(currApp);
|
||||
};
|
||||
|
||||
const testGETByProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(`/api/v1/${urlPath}/product?product=${currApp?.product}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject([currApp]);
|
||||
};
|
||||
|
||||
test.describe('GET /api/v1/identity-federation', () => {
|
||||
test('Fetch app by id', async ({ request }) => {
|
||||
await testGETById(oldUrlPath, request);
|
||||
await testGETById(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Fetch app by tenant and product', async ({ request }) => {
|
||||
await testGETByTenantProduct(oldUrlPath, request);
|
||||
await testGETByTenantProduct(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Fetch app by product', async ({ request }) => {
|
||||
await testGETByProduct(oldUrlPath, request);
|
||||
await testGETByProduct(newUrlPath, request);
|
||||
});
|
||||
});
|
||||
|
||||
const testPATCHById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.patch(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
id: currApp?.id,
|
||||
name: 'Updated App',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...currApp,
|
||||
name: 'Updated App',
|
||||
});
|
||||
};
|
||||
|
||||
const testPATCHByTenantProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.patch(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
tenant: currApp?.tenant,
|
||||
product: currApp?.product,
|
||||
name: 'Updated App 2',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...currApp,
|
||||
name: 'Updated App 2',
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('PATCH /api/v1/identity-federation', () => {
|
||||
test('Update app by id', async ({ request }) => {
|
||||
await testPATCHById(oldUrlPath, request);
|
||||
await testPATCHById(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Update app by tenant and product', async ({ request }) => {
|
||||
await testPATCHByTenantProduct(oldUrlPath, request);
|
||||
await testPATCHByTenantProduct(newUrlPath, request);
|
||||
});
|
||||
});
|
||||
|
||||
const testDELETEById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.delete(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({});
|
||||
|
||||
// Confirm app is deleted
|
||||
const response2 = await request.get(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
expect(response2.ok()).toBe(false);
|
||||
expect(response2.status()).toBe(404);
|
||||
};
|
||||
|
||||
test.describe('DELETE /api/v1/identity-federation', () => {
|
||||
test('Delete app by id', async ({ request }) => {
|
||||
await testDELETEById(oldUrlPath, request);
|
||||
await testDELETEById(newUrlPath, request);
|
||||
});
|
||||
});
|
|
@ -63,10 +63,14 @@ export class SSOPage {
|
|||
async addSSOConnection({
|
||||
name,
|
||||
type = 'saml',
|
||||
tenant,
|
||||
product,
|
||||
baseURL,
|
||||
}: {
|
||||
name: string;
|
||||
type: 'saml' | 'oidc';
|
||||
tenant?: string;
|
||||
product?: string;
|
||||
baseURL: string;
|
||||
}) {
|
||||
const connectionIndex = this.connections.length + 1;
|
||||
|
@ -80,9 +84,9 @@ export class SSOPage {
|
|||
// Fill the name for the connection
|
||||
await this.nameInput.fill(ssoName);
|
||||
// Fill the tenant for the connection
|
||||
await this.tenantInput.fill(ADMIN_PORTAL_TENANT);
|
||||
await this.tenantInput.fill(tenant || ADMIN_PORTAL_TENANT);
|
||||
// Fill the product for the connection
|
||||
await this.productInput.fill(ADMIN_PORTAL_PRODUCT);
|
||||
await this.productInput.fill(product || ADMIN_PORTAL_PRODUCT);
|
||||
// Fill the Allowed redirect URLs for the connection
|
||||
|
||||
await this.redirectURLSInput.fill(baseURL!);
|
||||
|
|
|
@ -48,6 +48,17 @@ test('OAuth2 wrapper + 2 OIDC providers', async ({ ssoPage, portal, page, baseUR
|
|||
// Login using MockLab
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-1`);
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-2`);
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
|
|
|
@ -48,6 +48,17 @@ test('OAuth2 wrapper + 2 SAML providers', async ({ ssoPage, portal, page, baseUR
|
|||
// Login using MockSAML
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-1`);
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-2`);
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
let oidcClientId;
|
||||
let oidcClientSecret;
|
||||
|
||||
const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page }, use) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
await use(ssoPage);
|
||||
// Delete SSO Connections mapped to OIDC federation
|
||||
await ssoPage.deleteSSOConnection('SSO-via-OIDC-Fed');
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
// Create OIDC Federated connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('button', { name: 'New App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/new$/);
|
||||
// Toggle connection type to OIDC
|
||||
await page.getByLabel('OIDC').check();
|
||||
await page.getByPlaceholder('Your app').and(page.getByLabel('Name')).fill('OF-1');
|
||||
await page.getByPlaceholder('example.com').and(page.getByLabel('Tenant')).fill('acme.com');
|
||||
await page.getByLabel('Product').fill('_jackson_admin_portal');
|
||||
await page.locator('input[name="item"]').fill('http://localhost:5225');
|
||||
await page.getByRole('button', { name: 'Create App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/.*\/edit$/);
|
||||
oidcClientId = await page
|
||||
.locator('label')
|
||||
.filter({ hasText: 'Client ID' })
|
||||
.getByRole('textbox')
|
||||
.inputValue();
|
||||
oidcClientSecret = await page
|
||||
.locator('label')
|
||||
.filter({ hasText: 'Client Secret' })
|
||||
.getByRole('textbox')
|
||||
.inputValue();
|
||||
await page.getByRole('link', { name: 'Back' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await expect(page.getByRole('cell', { name: 'OF-1' })).toBeVisible();
|
||||
|
||||
// Add OIDC Connection via OIDC Fed for Admin portal
|
||||
await page.getByRole('link', { name: 'Single Sign-On' }).click();
|
||||
await page.getByTestId('create-connection').click();
|
||||
await page.getByLabel('OIDC').check();
|
||||
await page.getByLabel('Connection name (Optional)').fill('SSO-via-OIDC-Fed');
|
||||
await page.getByLabel('Client ID').fill(oidcClientId);
|
||||
await page.getByLabel('Client Secret').fill(oidcClientSecret);
|
||||
await page
|
||||
.getByLabel('Well-known URL of OpenID Provider')
|
||||
.fill('http://localhost:5225/.well-known/openid-configuration');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByRole('cell', { name: 'SSO-via-OIDC-Fed' })).toBeVisible();
|
||||
await use(portal);
|
||||
// Delete Saml Fed connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('cell', { name: 'Edit' }).getByRole('button').click();
|
||||
await page.getByLabel('Card').getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByTestId('confirm-delete').click();
|
||||
},
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 SAML & 1 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 2 SAML providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockSAML-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-2');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 2 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-1');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 SAML provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 OIDC provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page }, use) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
await use(ssoPage);
|
||||
// Delete SSO Connections mapped to SAML federation
|
||||
await ssoPage.deleteSSOConnection('SSO-via-SAML-Fed');
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
// Create SAML Federated connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('button', { name: 'New App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/new$/);
|
||||
await page.getByPlaceholder('Your app').and(page.getByLabel('Name')).fill('SF-1');
|
||||
await page.getByPlaceholder('example.com').and(page.getByLabel('Tenant')).fill('acme.com');
|
||||
await page.getByLabel('Product').fill('_jackson_admin_portal');
|
||||
await page.getByLabel('ACS URL').fill('http://localhost:5225/api/oauth/saml');
|
||||
await page.getByLabel('Entity ID / Audience URI / Audience Restriction').fill('https://saml.boxyhq.com');
|
||||
await page.getByRole('button', { name: 'Create App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/.*\/edit$/);
|
||||
await page.getByRole('link', { name: 'Back' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await expect(page.getByRole('cell', { name: 'SF-1' })).toBeVisible();
|
||||
// Add SAML connection via SAML Fed for Admin portal
|
||||
await page.getByRole('link', { name: 'Single Sign-On' }).click();
|
||||
await page.getByTestId('create-connection').click();
|
||||
await page.getByLabel('Connection name (Optional)').fill('SSO-via-SAML-Fed');
|
||||
await page
|
||||
.getByPlaceholder('Paste the Metadata URL here')
|
||||
.fill('http://localhost:5225/.well-known/idp-metadata');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByRole('cell', { name: 'SSO-via-SAML-Fed' })).toBeVisible();
|
||||
await use(portal);
|
||||
// Delete Saml Fed connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('cell', { name: 'Edit' }).getByRole('button').click();
|
||||
await page.getByLabel('Card').getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByTestId('confirm-delete').click();
|
||||
},
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 SAML & 1 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 2 SAML providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockSAML-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-2');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 2 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-1');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 SAML provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 OIDC provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -18,19 +18,19 @@ const UpdateApp = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<LinkBack href='/admin/federated-saml' />
|
||||
<LinkBack href='/admin/identity-federation' />
|
||||
<EditFederatedSAMLApp
|
||||
urls={{
|
||||
getApp: `/api/admin/federated-saml/${id}`,
|
||||
updateApp: `/api/admin/federated-saml/${id}`,
|
||||
deleteApp: `/api/admin/federated-saml/${id}`,
|
||||
getApp: `/api/admin/identity-federation/${id}`,
|
||||
updateApp: `/api/admin/identity-federation/${id}`,
|
||||
deleteApp: `/api/admin/identity-federation/${id}`,
|
||||
}}
|
||||
onUpdate={() => {
|
||||
successToast(t('saml_federation_update_success'));
|
||||
successToast(t('identity_federation_update_success'));
|
||||
}}
|
||||
onDelete={() => {
|
||||
successToast(t('saml_federation_delete_success'));
|
||||
router.push('/admin/federated-saml');
|
||||
successToast(t('identity_federation_delete_success'));
|
||||
router.push('/admin/identity-federation');
|
||||
}}
|
||||
onError={(error) => {
|
||||
errorToast(error.message);
|
|
@ -9,10 +9,10 @@ const AppsList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
|
||||
return (
|
||||
<FederatedSAMLApps
|
||||
urls={{ getApps: '/api/admin/federated-saml' }}
|
||||
onEdit={(app) => router.push(`/admin/federated-saml/${app.id}/edit`)}
|
||||
urls={{ getApps: '/api/admin/identity-federation' }}
|
||||
onEdit={(app) => router.push(`/admin/identity-federation/${app.id}/edit`)}
|
||||
actions={{
|
||||
newApp: '/admin/federated-saml/new',
|
||||
newApp: '/admin/identity-federation/new',
|
||||
samlConfiguration: '/.well-known/idp-configuration',
|
||||
oidcConfiguration: '/.well-known/openid-configuration',
|
||||
}}
|
|
@ -1,12 +1,12 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationAppWithMetadata } from '@boxyhq/saml-jackson';
|
||||
import type { IdentityFederationAppWithMetadata } from '@boxyhq/saml-jackson';
|
||||
import { Toaster } from '@components/Toaster';
|
||||
import { InputWithCopyButton, CopyToClipboardButton, LinkOutline } from '@boxyhq/internal-ui';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
|
||||
type MetadataProps = {
|
||||
metadata: Pick<SAMLFederationAppWithMetadata, 'metadata'>['metadata'];
|
||||
metadata: Pick<IdentityFederationAppWithMetadata, 'metadata'>['metadata'];
|
||||
hasValidLicense: boolean;
|
||||
};
|
||||
|
|
@ -16,12 +16,12 @@ const NewApp = ({ hasValidLicense, samlAudience }: { hasValidLicense: boolean; s
|
|||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<LinkBack href='/admin/federated-saml' />
|
||||
<LinkBack href='/admin/identity-federation' />
|
||||
<NewFederatedSAMLApp
|
||||
urls={{ createApp: '/api/admin/federated-saml' }}
|
||||
urls={{ createApp: '/api/admin/identity-federation' }}
|
||||
onSuccess={(data) => {
|
||||
successToast(t('saml_federation_new_success'));
|
||||
router.replace(`/admin/federated-saml/${data.id}/edit`);
|
||||
successToast(t('identity_federation_new_success'));
|
||||
router.replace(`/admin/identity-federation/${data.id}/edit`);
|
||||
}}
|
||||
onError={(error) => {
|
||||
errorToast(error.message);
|
|
@ -1,3 +0,0 @@
|
|||
export { NewFederatedSAMLApp } from './NewFederatedSAMLApp';
|
||||
export { EditFederatedSAMLApp } from './EditFederatedSAMLApp';
|
||||
export { FederatedSAMLApps } from './FederatedSAMLApps';
|
|
@ -1,3 +1,4 @@
|
|||
export { usePaginate } from './usePaginate';
|
||||
export { useDirectory } from './useDirectory';
|
||||
export { useRouter } from './useRouter';
|
||||
export { useFetch } from './useFetch';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
async function parseResponseContent(response: Response) {
|
||||
const responseText = await response.text();
|
||||
|
||||
try {
|
||||
return responseText.length ? JSON.parse(responseText) : '';
|
||||
} catch (err) {
|
||||
return responseText;
|
||||
}
|
||||
}
|
||||
|
||||
export function useFetch<T>({ url }: { url: string }): {
|
||||
data?: T;
|
||||
isLoading: boolean;
|
||||
error: any;
|
||||
} {
|
||||
const [data, setData] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setIsLoading(true);
|
||||
const res = await fetch(url);
|
||||
setIsLoading(false);
|
||||
const resContent = await parseResponseContent(res);
|
||||
|
||||
if (res.ok) {
|
||||
const pageToken = res.headers.get('jackson-pagetoken');
|
||||
if (pageToken !== null) {
|
||||
setData({ ...resContent, pageToken });
|
||||
} else {
|
||||
setData(resContent);
|
||||
}
|
||||
} else {
|
||||
setError(resContent.error);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [url]);
|
||||
|
||||
return { data, isLoading, error };
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Button } from 'react-daisyui';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useFormik } from 'formik';
|
||||
|
@ -13,7 +13,7 @@ import { ItemList } from '@boxyhq/react-ui/shared';
|
|||
import { CopyToClipboardButton } from '../shared/InputWithCopyButton';
|
||||
import { IconButton } from '../shared/IconButton';
|
||||
|
||||
type EditApp = Pick<SAMLFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
|
||||
type EditApp = Pick<IdentityFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
|
||||
|
||||
export const Edit = ({
|
||||
app,
|
||||
|
@ -22,9 +22,9 @@ export const Edit = ({
|
|||
onUpdate,
|
||||
excludeFields,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
excludeFields?: 'product'[];
|
||||
}) => {
|
|
@ -1,12 +1,12 @@
|
|||
import { Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { useFormik } from 'formik';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { Card } from '../shared';
|
||||
import { AttributesMapping } from './AttributesMapping';
|
||||
|
||||
type Mappings = Pick<SAMLFederationApp, 'mappings'>;
|
||||
type Mappings = Pick<IdentityFederationApp, 'mappings'>;
|
||||
|
||||
export const EditAttributesMapping = ({
|
||||
app,
|
||||
|
@ -14,9 +14,9 @@ export const EditAttributesMapping = ({
|
|||
onUpdate,
|
||||
onError,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
|
@ -1,11 +1,11 @@
|
|||
import { Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { useFormik } from 'formik';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { Card } from '../shared';
|
||||
|
||||
type Branding = Pick<SAMLFederationApp, 'logoUrl' | 'faviconUrl' | 'primaryColor'>;
|
||||
type Branding = Pick<IdentityFederationApp, 'logoUrl' | 'faviconUrl' | 'primaryColor'>;
|
||||
|
||||
export const EditBranding = ({
|
||||
app,
|
||||
|
@ -13,9 +13,9 @@ export const EditBranding = ({
|
|||
onUpdate,
|
||||
onError,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
|
@ -1,5 +1,5 @@
|
|||
import useSWR from 'swr';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { EditBranding } from './EditBranding';
|
||||
import { Edit } from './Edit';
|
||||
import { EditAttributesMapping } from './EditAttributesMapping';
|
||||
|
@ -9,7 +9,7 @@ import { useEffect, useState } from 'react';
|
|||
import { defaultHeaders, fetcher } from '../utils';
|
||||
import { PageHeader } from '../shared';
|
||||
|
||||
export const EditFederatedSAMLApp = ({
|
||||
export const EditIdentityFederationApp = ({
|
||||
urls,
|
||||
onError,
|
||||
onUpdate,
|
||||
|
@ -17,7 +17,7 @@ export const EditFederatedSAMLApp = ({
|
|||
excludeFields,
|
||||
}: {
|
||||
urls: { getApp: string; updateApp: string; deleteApp: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onDelete?: () => void;
|
||||
excludeFields?: 'product'[];
|
||||
|
@ -25,7 +25,7 @@ export const EditFederatedSAMLApp = ({
|
|||
const { t } = useTranslation('common');
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const { data, isLoading, error, mutate } = useSWR<{ data: SAMLFederationApp }>(urls.getApp, fetcher);
|
||||
const { data, isLoading, error, mutate } = useSWR<{ data: IdentityFederationApp }>(urls.getApp, fetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
|
@ -1,5 +1,3 @@
|
|||
import useSWR from 'swr';
|
||||
import { fetcher } from '../utils';
|
||||
import {
|
||||
Loading,
|
||||
Table,
|
||||
|
@ -11,17 +9,17 @@ import {
|
|||
ButtonPrimary,
|
||||
} from '../shared';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||
import { TableBodyType } from '../shared/Table';
|
||||
import { pageLimit } from '../shared/Pagination';
|
||||
import { usePaginate } from '../hooks';
|
||||
import { useFetch, usePaginate } from '../hooks';
|
||||
import { useRouter } from '../hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type ExcludeFields = keyof Pick<SAMLFederationApp, 'product'>;
|
||||
type ExcludeFields = keyof Pick<IdentityFederationApp, 'product'>;
|
||||
|
||||
export const FederatedSAMLApps = ({
|
||||
export const IdentityFederationApps = ({
|
||||
urls,
|
||||
excludeFields,
|
||||
onEdit,
|
||||
|
@ -30,9 +28,9 @@ export const FederatedSAMLApps = ({
|
|||
}: {
|
||||
urls: { getApps: string };
|
||||
excludeFields?: ExcludeFields[];
|
||||
onEdit?: (app: SAMLFederationApp) => void;
|
||||
onEdit?: (app: IdentityFederationApp) => void;
|
||||
actions: { newApp: string; samlConfiguration: string; oidcConfiguration: string };
|
||||
actionCols?: { text: string; onClick: (app: SAMLFederationApp) => void; icon: JSX.Element }[];
|
||||
actionCols?: { text: string; onClick: (app: IdentityFederationApp) => void; icon: JSX.Element }[];
|
||||
}) => {
|
||||
const { router } = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
|
@ -45,10 +43,9 @@ export const FederatedSAMLApps = ({
|
|||
getAppsUrl += `&pageToken=${pageTokenMap[paginate.offset - pageLimit]}`;
|
||||
}
|
||||
|
||||
const { data, isLoading, error } = useSWR<{ data: SAMLFederationApp[]; pageToken?: string }>(
|
||||
getAppsUrl,
|
||||
fetcher
|
||||
);
|
||||
const { data, isLoading, error } = useFetch<{ data: IdentityFederationApp[]; pageToken?: string }>({
|
||||
url: getAppsUrl,
|
||||
});
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
|
@ -2,19 +2,19 @@ import { useFormik } from 'formik';
|
|||
import TagsInput from 'react-tagsinput';
|
||||
import { Card, Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import QuestionMarkCircleIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { AttributesMapping } from './AttributesMapping';
|
||||
import { PageHeader } from '../shared';
|
||||
import { ItemList } from '@boxyhq/react-ui/shared';
|
||||
|
||||
type NewSAMLFederationApp = Pick<
|
||||
SAMLFederationApp,
|
||||
type NewIdentityFederationApp = Pick<
|
||||
IdentityFederationApp,
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||
>;
|
||||
|
||||
export const NewFederatedSAMLApp = ({
|
||||
export const NewIdentityFederationApp = ({
|
||||
samlAudience = 'https://saml.boxyhq.com',
|
||||
urls,
|
||||
onSuccess,
|
||||
|
@ -24,14 +24,14 @@ export const NewFederatedSAMLApp = ({
|
|||
}: {
|
||||
samlAudience?: string;
|
||||
urls: { createApp: string };
|
||||
onSuccess?: (data: SAMLFederationApp) => void;
|
||||
onSuccess?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onEntityIdGenerated?: (entityId: string) => void;
|
||||
excludeFields?: 'product'[];
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const initialValues: NewSAMLFederationApp = {
|
||||
const initialValues: NewIdentityFederationApp = {
|
||||
type: 'saml',
|
||||
name: '',
|
||||
tenant: '',
|
||||
|
@ -44,11 +44,11 @@ export const NewFederatedSAMLApp = ({
|
|||
|
||||
if (excludeFields) {
|
||||
excludeFields.forEach((key) => {
|
||||
delete initialValues[key as keyof NewSAMLFederationApp];
|
||||
delete initialValues[key as keyof NewIdentityFederationApp];
|
||||
});
|
||||
}
|
||||
|
||||
const formik = useFormik<NewSAMLFederationApp>({
|
||||
const formik = useFormik<NewIdentityFederationApp>({
|
||||
initialValues: initialValues,
|
||||
onSubmit: async (values) => {
|
||||
const rawResponse = await fetch(urls.createApp, {
|
|
@ -0,0 +1,3 @@
|
|||
export { NewIdentityFederationApp as NewFederatedSAMLApp } from './NewIdentityFederationApp';
|
||||
export { EditIdentityFederationApp as EditFederatedSAMLApp } from './EditIdentityFederationApp';
|
||||
export { IdentityFederationApps as FederatedSAMLApps } from './IdentityFederationApps';
|
|
@ -1,7 +1,7 @@
|
|||
export * from './well-known';
|
||||
export * from './federated-saml';
|
||||
export * from './identity-federation';
|
||||
export * from './shared';
|
||||
export * from './dsync';
|
||||
export * from './provider';
|
||||
export * from './sso-tracer';
|
||||
export * from './sso-traces';
|
||||
export * from './setup-link';
|
||||
|
|
|
@ -10,7 +10,7 @@ import { addQueryParamsToPath, copyToClipboard, fetcher } from '../utils';
|
|||
import { TableBodyType } from '../shared/Table';
|
||||
import { pageLimit } from '../shared/Pagination';
|
||||
import { usePaginate, useRouter } from '../hooks';
|
||||
import type { SAMLFederationApp, SetupLink, SetupLinkService } from '../types';
|
||||
import type { IdentityFederationApp, SetupLink, SetupLinkService } from '../types';
|
||||
import {
|
||||
Loading,
|
||||
Table,
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
} from '../shared';
|
||||
import { SetupLinkInfoModal } from './SetupLinkInfoModal';
|
||||
|
||||
type ExcludeFields = keyof Pick<SAMLFederationApp, 'product'>;
|
||||
type ExcludeFields = keyof Pick<IdentityFederationApp, 'product'>;
|
||||
|
||||
export const SetupLinks = ({
|
||||
urls,
|
||||
|
|
|
@ -90,7 +90,7 @@ export type AttributeMapping = {
|
|||
value: string;
|
||||
};
|
||||
|
||||
export type SAMLFederationApp = {
|
||||
export type IdentityFederationApp = {
|
||||
id: string;
|
||||
type?: string;
|
||||
clientID?: string;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"apps": "Apps",
|
||||
"error_loading_page": "Unable to load this page. Maybe you don't have enough rights.",
|
||||
"documentation": "Documentation",
|
||||
"back": "Back",
|
||||
|
@ -29,9 +30,9 @@
|
|||
"send_magic_link": "Send Magic Link",
|
||||
"setup_links": "Setup Links",
|
||||
"edit_directory": "Edit Directory",
|
||||
"saml_federation_new_success": "Identity Federation app created successfully.",
|
||||
"saml_federation_update_success": "Identity Federation app updated successfully.",
|
||||
"saml_federation_delete_success": "Identity federation app deleted successfully",
|
||||
"identity_federation_new_success": "Identity Federation app created successfully.",
|
||||
"identity_federation_update_success": "Identity Federation app updated successfully.",
|
||||
"identity_federation_delete_success": "Identity Federation app deleted successfully",
|
||||
"saml_federation_app_info": "SAML Federation App Information",
|
||||
"saml_federation_app_info_details": "Choose from the following options to configure your SAML Federation on the service provider side",
|
||||
"download_metadata": "Download Metadata",
|
||||
|
@ -41,7 +42,7 @@
|
|||
"directory_created_successfully": "Directory created successfully",
|
||||
"directory_updated_successfully": "Directory updated successfully",
|
||||
"dashboard": "Dashboard",
|
||||
"saml_federation": "Identity Federation",
|
||||
"identity_federation": "Identity Federation",
|
||||
"settings": "Settings",
|
||||
"admin_portal_sso": "SSO for Admin Portal",
|
||||
"configuration": "Configuration",
|
||||
|
@ -224,7 +225,7 @@
|
|||
"bui-dsync-authorization-google": "Authorize Google Workspace",
|
||||
"bui-dsync-authorization-google-desc": "You should authorize Google Workspace to sync your directory. Click the button below start the authorization process. Make sure you have the necessary permissions to authorize Google Workspace.",
|
||||
"bui-dsync-google-domain": "Google Domain",
|
||||
"bui-tracer-title": "SSO Tracer",
|
||||
"bui-tracer-title": "SSO Traces",
|
||||
"bui-tracer-id": "Trace ID",
|
||||
"bui-tracer-description": "Description",
|
||||
"bui-tracer-assertion-type": "Assertion Type",
|
||||
|
|
|
@ -11,6 +11,7 @@ const unAuthenticatedApiRoutes = [
|
|||
'/api/hello',
|
||||
'/api/auth/**',
|
||||
'/api/federated-saml/**',
|
||||
'/api/identity-federation/**',
|
||||
'/api/logout/**',
|
||||
'/api/oauth/**',
|
||||
'/api/scim/v2.0/**',
|
||||
|
|
|
@ -101,6 +101,10 @@ module.exports = {
|
|||
source: '/api/v1/saml-traces/:path*',
|
||||
destination: '/api/v1/sso-traces/:path*',
|
||||
},
|
||||
{
|
||||
source: '/api/v1/federated-saml/:path*',
|
||||
destination: '/api/v1/identity-federation/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
|
|
@ -26,8 +26,8 @@ const map = {
|
|||
'src/directory-sync/request.ts',
|
||||
],
|
||||
'test/dsync/events.test.ts': ['src/directory-sync/events.ts'],
|
||||
'test/federated-saml/app.test.ts': ['src/ee/federated-saml/app.ts'],
|
||||
'test/federated-saml/sso.test.ts': ['src/ee/federated-saml/sso.ts'],
|
||||
'test/identity-federation/app.test.ts': ['src/ee/identity-federation/app.ts'],
|
||||
'test/identity-federation/sso.test.ts': ['src/ee/identity-federation/sso.ts'],
|
||||
'test/event/index.test.ts': ['src/event/*'],
|
||||
'test/dsync/google_oauth.test.ts': [
|
||||
'src/directory-sync/non-scim/google/oauth.ts',
|
||||
|
|
|
@ -21,7 +21,7 @@ import type {
|
|||
SSOTracerInstance,
|
||||
OAuthErrorHandlerParams,
|
||||
OIDCAuthzResponsePayload,
|
||||
SAMLFederationApp,
|
||||
IdentityFederationApp,
|
||||
} from '../typings';
|
||||
import {
|
||||
relayStatePrefix,
|
||||
|
@ -45,7 +45,7 @@ import { getDefaultCertificate } from '../saml/x509';
|
|||
import { SSOHandler } from './sso-handler';
|
||||
import { ValidateOption, extractSAMLResponseAttributes } from '../saml/lib';
|
||||
import { oidcIssuerInstance } from './oauth/oidc-issuer';
|
||||
import { App } from '../ee/federated-saml/app';
|
||||
import { App } from '../ee/identity-federation/app';
|
||||
|
||||
const deflateRawAsync = promisify(deflateRaw);
|
||||
|
||||
|
@ -96,7 +96,7 @@ export class OAuthController implements IOAuthController {
|
|||
let requestedOIDCFlow: boolean | undefined;
|
||||
let isOIDCFederated: boolean | undefined;
|
||||
let connection: SAMLSSORecord | OIDCSSORecord | undefined;
|
||||
let fedApp: SAMLFederationApp | undefined;
|
||||
let fedApp: IdentityFederationApp | undefined;
|
||||
|
||||
try {
|
||||
const tenant = 'tenant' in body ? body.tenant : undefined;
|
||||
|
|
|
@ -5,7 +5,13 @@ import { deflateRaw } from 'zlib';
|
|||
import type { SAMLProfile } from '@boxyhq/saml20/dist/typings';
|
||||
import { generators } from 'openid-client';
|
||||
|
||||
import type { JacksonOption, Storable, SAMLSSORecord, OIDCSSORecord, SAMLFederationApp } from '../typings';
|
||||
import type {
|
||||
JacksonOption,
|
||||
Storable,
|
||||
SAMLSSORecord,
|
||||
OIDCSSORecord,
|
||||
IdentityFederationApp,
|
||||
} from '../typings';
|
||||
import { getDefaultCertificate } from '../saml/x509';
|
||||
import * as dbutils from '../db/utils';
|
||||
import { JacksonError } from './error';
|
||||
|
@ -167,7 +173,7 @@ export class SSOHandler {
|
|||
}: {
|
||||
connection: SAMLSSORecord;
|
||||
requestParams: Record<string, any>;
|
||||
mappings: SAMLFederationApp['mappings'];
|
||||
mappings: IdentityFederationApp['mappings'];
|
||||
}) {
|
||||
// We have a connection now, so we can create the SAML request
|
||||
const certificate = await getDefaultCertificate();
|
||||
|
@ -240,7 +246,7 @@ export class SSOHandler {
|
|||
}: {
|
||||
connection: OIDCSSORecord;
|
||||
requestParams: Record<string, any>;
|
||||
mappings: SAMLFederationApp['mappings'];
|
||||
mappings: IdentityFederationApp['mappings'];
|
||||
}) {
|
||||
if (!this.opts.oidcPath) {
|
||||
throw new JacksonError('OpenID response handler path (oidcPath) is not set', 400);
|
||||
|
@ -347,7 +353,7 @@ export class SSOHandler {
|
|||
requested: any;
|
||||
oidcCodeVerifier?: string;
|
||||
oidcNonce?: string;
|
||||
mappings: SAMLFederationApp['mappings'];
|
||||
mappings: IdentityFederationApp['mappings'];
|
||||
}) => {
|
||||
const sessionId = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import saml from '@boxyhq/saml20';
|
|||
import type {
|
||||
Storable,
|
||||
JacksonOption,
|
||||
SAMLFederationApp,
|
||||
IdentityFederationApp,
|
||||
Records,
|
||||
GetByProductParams,
|
||||
AppRequestParams,
|
||||
|
@ -17,7 +17,7 @@ import { IndexNames, validateTenantAndProduct } from '../../controller/utils';
|
|||
import { throwIfInvalidLicense } from '../common/checkLicense';
|
||||
|
||||
type NewAppParams = Pick<
|
||||
SAMLFederationApp,
|
||||
IdentityFederationApp,
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||
> & {
|
||||
logoUrl?: string;
|
||||
|
@ -32,7 +32,7 @@ export class App {
|
|||
/**
|
||||
* @swagger
|
||||
* definitions:
|
||||
* SAMLFederationApp:
|
||||
* IdentityFederationApp:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
|
@ -71,7 +71,7 @@ export class App {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* /api/v1/identity-federation:
|
||||
* post:
|
||||
* summary: Create an Identity Federation app
|
||||
* parameters:
|
||||
|
@ -147,7 +147,7 @@ export class App {
|
|||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/definitions/SAMLFederationApp'
|
||||
* $ref: '#/definitions/IdentityFederationApp'
|
||||
*/
|
||||
public async create({
|
||||
name,
|
||||
|
@ -201,7 +201,7 @@ export class App {
|
|||
value: entityId,
|
||||
});
|
||||
|
||||
const apps: SAMLFederationApp[] = result.data;
|
||||
const apps: IdentityFederationApp[] = result.data;
|
||||
|
||||
if (apps && apps.length > 0) {
|
||||
throw new JacksonError(
|
||||
|
@ -219,7 +219,7 @@ export class App {
|
|||
_tenants.push(tenant);
|
||||
}
|
||||
|
||||
const app: SAMLFederationApp = {
|
||||
const app: IdentityFederationApp = {
|
||||
id,
|
||||
type,
|
||||
redirectUrl,
|
||||
|
@ -261,7 +261,7 @@ export class App {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* /api/v1/identity-federation:
|
||||
* get:
|
||||
* summary: Get an Identity Federation app
|
||||
* parameters:
|
||||
|
@ -288,7 +288,7 @@ export class App {
|
|||
* '200':
|
||||
* description: Success
|
||||
* schema:
|
||||
* $ref: '#/definitions/SAMLFederationApp'
|
||||
* $ref: '#/definitions/IdentityFederationApp'
|
||||
*/
|
||||
public async get(params: AppRequestParams) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
@ -300,7 +300,7 @@ export class App {
|
|||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
return app as SAMLFederationApp;
|
||||
return app as IdentityFederationApp;
|
||||
}
|
||||
|
||||
if ('tenant' in params && 'product' in params) {
|
||||
|
@ -310,7 +310,7 @@ export class App {
|
|||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
return app as SAMLFederationApp;
|
||||
return app as IdentityFederationApp;
|
||||
}
|
||||
|
||||
throw new JacksonError('Provide either the `id` or `tenant` and `product` to get the app', 400);
|
||||
|
@ -318,7 +318,7 @@ export class App {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/federated-saml/product:
|
||||
* /api/v1/identity-federation/product:
|
||||
* get:
|
||||
* summary: Get Identity Federation apps by product
|
||||
* parameters:
|
||||
|
@ -345,7 +345,7 @@ export class App {
|
|||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/definitions/SAMLFederationApp'
|
||||
* $ref: '#/definitions/IdentityFederationApp'
|
||||
* pageToken:
|
||||
* type: string
|
||||
* description: token for pagination
|
||||
|
@ -378,7 +378,7 @@ export class App {
|
|||
throw new JacksonError('Missing required parameters. Required parameters are: entityId', 400);
|
||||
}
|
||||
|
||||
const apps: SAMLFederationApp[] = (
|
||||
const apps: IdentityFederationApp[] = (
|
||||
await this.store.getByIndex({
|
||||
name: IndexNames.EntityID,
|
||||
value: entityId,
|
||||
|
@ -394,7 +394,7 @@ export class App {
|
|||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* /api/v1/identity-federation:
|
||||
* patch:
|
||||
* summary: Update an Identity Federation app
|
||||
* parameters:
|
||||
|
@ -459,9 +459,9 @@ export class App {
|
|||
* '200':
|
||||
* description: Success
|
||||
* schema:
|
||||
* $ref: '#/definitions/SAMLFederationApp'
|
||||
* $ref: '#/definitions/IdentityFederationApp'
|
||||
*/
|
||||
public async update(params: Partial<SAMLFederationApp>) {
|
||||
public async update(params: Partial<IdentityFederationApp>) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const { id, tenant, product, type } = params;
|
||||
|
@ -470,7 +470,7 @@ export class App {
|
|||
throw new JacksonError('Provide either the `id` or `tenant` and `product` to update the app', 400);
|
||||
}
|
||||
|
||||
let app: null | SAMLFederationApp = null;
|
||||
let app: null | IdentityFederationApp = null;
|
||||
|
||||
if (id) {
|
||||
app = await this.get({ id });
|
||||
|
@ -482,7 +482,7 @@ export class App {
|
|||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
const toUpdate: Partial<SAMLFederationApp> = {};
|
||||
const toUpdate: Partial<IdentityFederationApp> = {};
|
||||
|
||||
// Support partial updates
|
||||
|
||||
|
@ -556,14 +556,18 @@ export class App {
|
|||
}) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const apps = (await this.store.getAll(pageOffset, pageLimit, pageToken)) as Records<SAMLFederationApp>;
|
||||
const apps = (await this.store.getAll(
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken
|
||||
)) as Records<IdentityFederationApp>;
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* /api/v1/identity-federation:
|
||||
* delete:
|
||||
* summary: Delete an Identity Federation app
|
||||
* parameters:
|
||||
|
@ -590,7 +594,7 @@ export class App {
|
|||
* '200':
|
||||
* description: Success
|
||||
* schema:
|
||||
* $ref: '#/definitions/SAMLFederationApp'
|
||||
* $ref: '#/definitions/IdentityFederationApp'
|
||||
*/
|
||||
public async delete(params: AppRequestParams): Promise<void> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
@ -613,7 +617,7 @@ export class App {
|
|||
|
||||
const { publicKey } = await getDefaultCertificate();
|
||||
|
||||
const ssoUrl = `${this.opts.externalUrl}/api/federated-saml/sso`;
|
||||
const ssoUrl = `${this.opts.externalUrl}/api/identity-federation/sso`;
|
||||
const entityId = `${this.opts.samlAudience}`;
|
||||
|
||||
const xml = saml.createIdPMetadataXML({
|
|
@ -4,7 +4,7 @@ import type { JacksonOption, SSOTracerInstance } from '../../typings';
|
|||
import { SSOHandler } from '../../controller/sso-handler';
|
||||
|
||||
// This is the main entry point for the Identity Federation module
|
||||
const SAMLFederation = async ({
|
||||
const IdentityFederation = async ({
|
||||
db,
|
||||
opts,
|
||||
ssoTracer,
|
||||
|
@ -34,7 +34,7 @@ const SAMLFederation = async ({
|
|||
return response;
|
||||
};
|
||||
|
||||
export default SAMLFederation;
|
||||
export default IdentityFederation;
|
||||
|
||||
export * from './types';
|
||||
|
|
@ -6,7 +6,7 @@ import { SSOHandler } from '../../controller/sso-handler';
|
|||
import type {
|
||||
JacksonOption,
|
||||
OIDCSSORecord,
|
||||
SAMLFederationApp,
|
||||
IdentityFederationApp,
|
||||
SAMLSSORecord,
|
||||
SSOTracerInstance,
|
||||
} from '../../typings';
|
||||
|
@ -56,7 +56,7 @@ export class SSO {
|
|||
|
||||
const isPostBinding = samlBinding === 'HTTP-POST';
|
||||
let connection: SAMLSSORecord | OIDCSSORecord | undefined;
|
||||
let app: SAMLFederationApp | undefined;
|
||||
let app: IdentityFederationApp | undefined;
|
||||
let id, acsUrl, entityId, publicKey, providerName, decodedRequest;
|
||||
|
||||
try {
|
|
@ -1,13 +1,13 @@
|
|||
import SAMLFederation from '.';
|
||||
import IdentityFederation from '.';
|
||||
|
||||
export type ISAMLFederationController = Awaited<ReturnType<typeof SAMLFederation>>;
|
||||
export type IIdentityFederationController = Awaited<ReturnType<typeof IdentityFederation>>;
|
||||
|
||||
export type AttributeMapping = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type SAMLFederationApp = {
|
||||
export type IdentityFederationApp = {
|
||||
id: string;
|
||||
type?: string;
|
||||
clientID?: string;
|
||||
|
@ -25,7 +25,7 @@ export type SAMLFederationApp = {
|
|||
mappings?: AttributeMapping[] | null;
|
||||
};
|
||||
|
||||
export type SAMLFederationAppWithMetadata = SAMLFederationApp & {
|
||||
export type IdentityFederationAppWithMetadata = IdentityFederationApp & {
|
||||
metadata: {
|
||||
entityId: string;
|
||||
ssoUrl: string;
|
|
@ -13,10 +13,10 @@ import { SPSSOConfig } from './controller/sp-config';
|
|||
import { SetupLinkController } from './controller/setup-link';
|
||||
import { AnalyticsController } from './controller/analytics';
|
||||
import * as x509 from './saml/x509';
|
||||
import initFederatedSAML, { type ISAMLFederationController } from './ee/federated-saml';
|
||||
import initFederatedSAML, { type IIdentityFederationController } from './ee/identity-federation';
|
||||
import checkLicense from './ee/common/checkLicense';
|
||||
import { BrandingController } from './ee/branding';
|
||||
import SSOTracer from './sso-tracer';
|
||||
import SSOTracer from './sso-traces';
|
||||
import EventController from './event';
|
||||
import { ProductController } from './ee/product';
|
||||
import { OryController } from './ee/ory/ory';
|
||||
|
@ -70,7 +70,7 @@ export const controllers = async (
|
|||
directorySyncController: IDirectorySyncController;
|
||||
oidcDiscoveryController: OidcDiscoveryController;
|
||||
spConfig: SPSSOConfig;
|
||||
samlFederatedController: ISAMLFederationController;
|
||||
samlFederatedController: IIdentityFederationController;
|
||||
brandingController: IBrandingController;
|
||||
checkLicense: () => Promise<boolean>;
|
||||
productController: ProductController;
|
||||
|
@ -193,7 +193,7 @@ export const controllers = async (
|
|||
export default controllers;
|
||||
|
||||
export * from './typings';
|
||||
export * from './ee/federated-saml/types';
|
||||
export * from './ee/identity-federation/types';
|
||||
export type SAMLJackson = Awaited<ReturnType<typeof controllers>>;
|
||||
export type ISetupLinkController = InstanceType<typeof SetupLinkController>;
|
||||
export type IBrandingController = InstanceType<typeof BrandingController>;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { JWK } from 'jose';
|
||||
import type { CallbackParamsType, IssuerMetadata } from 'openid-client';
|
||||
|
||||
export * from './ee/federated-saml/types';
|
||||
export * from './sso-tracer/types';
|
||||
export * from './ee/identity-federation/types';
|
||||
export * from './sso-traces/types';
|
||||
export * from './directory-sync/types';
|
||||
export * from './event/types';
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="ONELOGIN_082f89d9-a32a-441d-ae49-ab6fc13fe73b" Version="2.0" IssueInstant="2022-11-29T09:04:48Z" ProviderName="Twilio" Destination="http://localhost:5225/api/federated-saml/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://twilio.com/saml2/acs"><saml:Issuer>https://twilio.com/saml2/entityId</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true" /><samlp:RequestedAuthnContext Comparison="exact"><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>
|
|
@ -1,11 +1,11 @@
|
|||
import tap from 'tap';
|
||||
|
||||
import { ISAMLFederationController } from '../../src';
|
||||
import { IIdentityFederationController } from '../../src';
|
||||
import { jacksonOptions } from '../utils';
|
||||
import { tenant, product, serviceProvider, appId } from './constants';
|
||||
import { getDefaultCertificate } from '../../src/saml/x509';
|
||||
|
||||
let samlFederatedController: ISAMLFederationController;
|
||||
let samlFederatedController: IIdentityFederationController;
|
||||
|
||||
tap.before(async () => {
|
||||
const jackson = await (await import('../../src/index')).default(jacksonOptions);
|
||||
|
@ -149,7 +149,7 @@ tap.test('Federated SAML App', async () => {
|
|||
|
||||
t.ok(response);
|
||||
t.match(response.entityId, jacksonOptions.samlAudience);
|
||||
t.match(response.ssoUrl, `${jacksonOptions.externalUrl}/api/federated-saml/sso`);
|
||||
t.match(response.ssoUrl, `${jacksonOptions.externalUrl}/api/identity-federation/sso`);
|
||||
t.match(response.x509cert, certs.publicKey);
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="ONELOGIN_082f89d9-a32a-441d-ae49-ab6fc13fe73b" Version="2.0" IssueInstant="2022-11-29T09:04:48Z" ProviderName="Twilio" Destination="http://localhost:5225/api/identity-federation/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://twilio.com/saml2/acs"><saml:Issuer>https://twilio.com/saml2/entityId</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true" /><samlp:RequestedAuthnContext Comparison="exact"><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>
|
|
@ -11,18 +11,18 @@ const deflateRawAsync = promisify(deflateRaw);
|
|||
import { jacksonOptions } from '../utils';
|
||||
import { tenant, product, serviceProvider } from './constants';
|
||||
import type {
|
||||
ISAMLFederationController,
|
||||
IIdentityFederationController,
|
||||
IConnectionAPIController,
|
||||
IOAuthController,
|
||||
SAMLFederationApp,
|
||||
IdentityFederationApp,
|
||||
SAMLSSORecord,
|
||||
} from '../../src';
|
||||
|
||||
let oauthController: IOAuthController;
|
||||
let samlFederatedController: ISAMLFederationController;
|
||||
let samlFederatedController: IIdentityFederationController;
|
||||
let connectionAPIController: IConnectionAPIController;
|
||||
|
||||
let app: SAMLFederationApp;
|
||||
let app: IdentityFederationApp;
|
||||
let connection: SAMLSSORecord;
|
||||
|
||||
tap.before(async () => {
|
|
@ -1,5 +1,5 @@
|
|||
import tap from 'tap';
|
||||
import SSOTracer from '../../src/sso-tracer';
|
||||
import SSOTracer from '../../src/sso-traces';
|
||||
import { jacksonOptions } from '../utils';
|
||||
import DB from '../../src/db/db';
|
||||
|
|
@ -2,7 +2,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/federated-saml/pages/edit';
|
||||
export { default } from '@ee/identity-federation/pages/edit';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
|
@ -2,7 +2,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/federated-saml/pages/index';
|
||||
export { default } from '@ee/identity-federation/pages/index';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
|
@ -3,7 +3,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|||
import jackson from '@lib/jackson';
|
||||
import { jacksonOptions } from '@lib/env';
|
||||
|
||||
export { default } from 'ee/federated-saml/pages/new';
|
||||
export { default } from '@ee/identity-federation/pages/new';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
|
@ -10,8 +10,8 @@ const SSOTraceInspector: NextPage = () => {
|
|||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<LinkBack href='/admin/sso-tracer' />
|
||||
<SSOTracerInfo urls={{ getTracer: `/api/admin/sso-tracer/${traceId}` }} />
|
||||
<LinkBack href='/admin/sso-traces' />
|
||||
<SSOTracerInfo urls={{ getTracer: `/api/admin/sso-traces/${traceId}` }} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -8,8 +8,8 @@ const SSOTraceViewer: NextPage = () => {
|
|||
|
||||
return (
|
||||
<SSOTracers
|
||||
urls={{ getTracers: '/api/admin/sso-tracer' }}
|
||||
onView={(trace) => router.push(`/admin/sso-tracer/${trace.traceId}/inspect`)}
|
||||
urls={{ getTracers: '/api/admin/sso-traces' }}
|
||||
onView={(trace) => router.push(`/admin/sso-traces/${trace.traceId}/inspect`)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { default } from 'ee/federated-saml/api/admin/[id]/index';
|
|
@ -1 +0,0 @@
|
|||
export { default } from 'ee/federated-saml/api/admin/index';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@ee/identity-federation/api/admin/[id]/index';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@ee/identity-federation/api/admin/index';
|
|
@ -1 +1 @@
|
|||
export { default } from 'ee/federated-saml/api/sso';
|
||||
export { default } from '@ee/identity-federation/api/sso';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@ee/identity-federation/api/sso';
|
|
@ -1 +0,0 @@
|
|||
export { default } from 'ee/federated-saml/api/v1/index';
|
|
@ -1 +0,0 @@
|
|||
export { default } from 'ee/federated-saml/api/v1/product';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@ee/identity-federation/api/v1/index';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@ee/identity-federation/api/v1/product';
|
|
@ -1 +1 @@
|
|||
export { default } from 'ee/federated-saml/api/metadata';
|
||||
export { default } from '@ee/identity-federation/api/metadata';
|
||||
|
|
|
@ -203,7 +203,7 @@ export const getServerSideProps = async ({ query, locale, req }) => {
|
|||
const params = new URLSearchParams(paramsToRelay);
|
||||
const destination =
|
||||
samlFedAppId && fedType !== 'oidc'
|
||||
? `/api/federated-saml/sso?${params}`
|
||||
? `/api/identity-federation/sso?${params}`
|
||||
: `/api/oauth/authorize?${params}`;
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/federated-saml/pages/metadata';
|
||||
export { default } from '@ee/identity-federation/pages/metadata';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { samlFederatedController, checkLicense } = await jackson();
|
||||
|
|
|
@ -6,11 +6,11 @@ const config: PlaywrightTestConfig = {
|
|||
workers: 1,
|
||||
globalSetup: require.resolve('./e2e/support/globalSetup'),
|
||||
// Timeout per test
|
||||
timeout: 30 * 1000,
|
||||
timeout: 100 * 1000,
|
||||
// Test directory
|
||||
testDir: path.join(__dirname, 'e2e'),
|
||||
// If a test fails, retry it additional 2 times
|
||||
retries: 0,
|
||||
// If a test fails, retry it additional 3 times
|
||||
retries: 3,
|
||||
// Artifacts folder where screenshots, videos, and traces are stored.
|
||||
outputDir: 'test-results/',
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Enterprise SSO & Directory Sync",
|
||||
"version": "1.20.6",
|
||||
"version": "1.23.5",
|
||||
"description": "This is the API documentation for SAML Jackson service.",
|
||||
"termsOfService": "https://boxyhq.com/terms.html",
|
||||
"contact": {
|
||||
|
@ -1354,7 +1354,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/federated-saml": {
|
||||
"/api/v1/identity-federation": {
|
||||
"post": {
|
||||
"summary": "Create an Identity Federation app",
|
||||
"parameters": [
|
||||
|
@ -1459,7 +1459,7 @@
|
|||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SAMLFederationApp"
|
||||
"$ref": "#/definitions/IdentityFederationApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1500,7 +1500,7 @@
|
|||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SAMLFederationApp"
|
||||
"$ref": "#/definitions/IdentityFederationApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1593,7 +1593,7 @@
|
|||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SAMLFederationApp"
|
||||
"$ref": "#/definitions/IdentityFederationApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1633,13 +1633,13 @@
|
|||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SAMLFederationApp"
|
||||
"$ref": "#/definitions/IdentityFederationApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/federated-saml/product": {
|
||||
"/api/v1/identity-federation/product": {
|
||||
"get": {
|
||||
"summary": "Get Identity Federation apps by product",
|
||||
"parameters": [
|
||||
|
@ -1677,7 +1677,7 @@
|
|||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SAMLFederationApp"
|
||||
"$ref": "#/definitions/IdentityFederationApp"
|
||||
}
|
||||
},
|
||||
"pageToken": {
|
||||
|
@ -2034,7 +2034,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"SAMLFederationApp": {
|
||||
"IdentityFederationApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
Loading…
Reference in New Issue