mirror of https://github.com/boxyhq/jackson.git
New OIDC fed (#2336)
* add WellKnownURLs * Fix translation keys * Update dependencies and add IdP Configuration * Update common.json with new translations * wip * Update @boxyhq/internal-ui version to 0.0.5 * add internal ui folder * Fix imports and build * Refactor internal-ui package structure * wip shared UI * Fix the build * WIP * Add new components and hooks for directory sync * WIP * lint fix * updated swr * WIP * users * Refactor shared components and fix API endpoints*** ***Update directory user page and add new federated SAML app * Fix lint * wip * Add new files and update existing files * Refactor DirectoryGroups and DirectoryInfo components * Update localization strings for directory UI * Update Google Auth URL description in common.json * Refactor directory tab and add delete functionality to webhook logs * IdP selection screen changes * Delete unused files and update dependencies * Fix column declaration * Add internal-ui/dist to .gitignore * Update page limit and add new dependencies * wip * Refactor directory search in user API endpoint * wip * Refactor directory retrieval logic in user and group API handlers * Add API endpoints for retrieving webhook events * check app's redirectUrl, TODO: save app info into session to read later * Add query parameters to API URLs in DirectoryGroups * working saml login via IdP select. TODO: oidc login via IdP select and saml + oidc login with 1 connection * oidc IdP working with selection * working oidc fed -> saml flow * Add Google authorization status badge and handle pagination in FederatedSAMLApps * Add router prop to AppsList component and update page header titles * UI changes * updated peer-deps * Add new files and export functions * Remove unused router prop * Add PencilIcon to FederatedSAMLApps * updated federated app creation page * updated federated app edit page * Refactor FederatedSAMLApps and NewFederatedSAMLApp components * lint fix * lint fix * updated package-lock * add jose npm to dev dep * added missing strings * added missing strings * locale strings fix * locale strings cleanup * tweaks to icon imports * replaced textarea with list of inputs for Federated Apps redirect url * update package-lock * Add prepublish step * Build and publish npm and internal ui * Refactor install step * Run npm install (for local) inside internal ui automatically using prepare * Remove eslint setup for internal-ui * updated package-lock * Add `--legacy-peer-deps` to prevent installing peer dependencies * Fix the types import path * wip * wip * Fix the types * Format * Update package-lock * Cleanup * Try adding jose library version 5.2.2 * allow selective subdomain globbing * removed duplicate jose lib * updated package-lock * updated swagger doc * SAML Federation -> Identity Federation * fixed locale strings * turn off autocomplete for tags input --------- Co-authored-by: Kiran K <mailtokirankk@gmail.com> Co-authored-by: Aswin V <vaswin91@gmail.com>
This commit is contained in:
parent
f923451fbf
commit
a473b360ef
|
@ -8,7 +8,6 @@ const allStrings = {};
|
||||||
const localeFile = require('./locales/en/common.json');
|
const localeFile = require('./locales/en/common.json');
|
||||||
|
|
||||||
const files = fs.readdirSync('./', { recursive: true, withFileTypes: true });
|
const files = fs.readdirSync('./', { recursive: true, withFileTypes: true });
|
||||||
//console.log('files:', files);
|
|
||||||
|
|
||||||
let error = false;
|
let error = false;
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ files.forEach((file) => {
|
||||||
|
|
||||||
(fileContent.match(regExp) || []).forEach((match) => {
|
(fileContent.match(regExp) || []).forEach((match) => {
|
||||||
const id = match.replace("t('", '').replace("'", '');
|
const id = match.replace("t('", '').replace("'", '');
|
||||||
// console.log('match:', match);
|
|
||||||
allStrings[id] = true;
|
allStrings[id] = true;
|
||||||
if (!localeFile[id]) {
|
if (!localeFile[id]) {
|
||||||
error = true;
|
error = true;
|
||||||
|
@ -35,7 +33,6 @@ files.forEach((file) => {
|
||||||
|
|
||||||
(fileContent.match(altRegExp) || []).forEach((match) => {
|
(fileContent.match(altRegExp) || []).forEach((match) => {
|
||||||
const id = match.replace("i18nKey='", '').replace("'", '');
|
const id = match.replace("i18nKey='", '').replace("'", '');
|
||||||
// console.log('match:', match, id);
|
|
||||||
allStrings[id] = true;
|
allStrings[id] = true;
|
||||||
if (!localeFile[id]) {
|
if (!localeFile[id]) {
|
||||||
error = true;
|
error = true;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import SSOLogo from '@components/logo/SSO';
|
||||||
import DSyncLogo from '@components/logo/DSync';
|
import DSyncLogo from '@components/logo/DSync';
|
||||||
import AuditLogsLogo from '@components/logo/AuditLogs';
|
import AuditLogsLogo from '@components/logo/AuditLogs';
|
||||||
import Vault from '@components/logo/Vault';
|
import Vault from '@components/logo/Vault';
|
||||||
import { Cog8ToothIcon } from '@heroicons/react/24/outline';
|
import Cog8ToothIcon from '@heroicons/react/24/outline/Cog8ToothIcon';
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { ButtonLink } from '@components/ButtonLink';
|
||||||
import { Dispatch, FormEvent, SetStateAction, useMemo, useState } from 'react';
|
import { Dispatch, FormEvent, SetStateAction, useMemo, useState } from 'react';
|
||||||
import { EditViewOnlyFields, getCommonFields } from './fieldCatalog';
|
import { EditViewOnlyFields, getCommonFields } from './fieldCatalog';
|
||||||
import { CopyToClipboardButton } from '@components/ClipboardButton';
|
import { CopyToClipboardButton } from '@components/ClipboardButton';
|
||||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
|
||||||
|
import EyeSlashIcon from '@heroicons/react/24/outline/EyeSlashIcon';
|
||||||
import { IconButton } from '@components/IconButton';
|
import { IconButton } from '@components/IconButton';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get SAML Federation app by id
|
// Get Identity Federation app by id
|
||||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { samlFederatedController } = await jackson();
|
const { samlFederatedController } = await jackson();
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update SAML Federation app
|
// Update Identity Federation app
|
||||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { samlFederatedController } = await jackson();
|
const { samlFederatedController } = await jackson();
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
return res.status(200).json({ data: updatedApp });
|
return res.status(200).json({ data: updatedApp });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete the SAML Federation app
|
// Delete the Identity Federation app
|
||||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { samlFederatedController } = await jackson();
|
const { samlFederatedController } = await jackson();
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create new SAML Federation app
|
// Create new Identity Federation app
|
||||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { samlFederatedController } = await jackson();
|
const { samlFederatedController } = await jackson();
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
return res.status(201).json({ data: app });
|
return res.status(201).json({ data: app });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get SAML Federation apps
|
// Get Identity Federation apps
|
||||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { samlFederatedController } = await jackson();
|
const { samlFederatedController } = await jackson();
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,11 @@ const AppsList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||||
<FederatedSAMLApps
|
<FederatedSAMLApps
|
||||||
urls={{ getApps: '/api/admin/federated-saml' }}
|
urls={{ getApps: '/api/admin/federated-saml' }}
|
||||||
onEdit={(app) => router.push(`/admin/federated-saml/${app.id}/edit`)}
|
onEdit={(app) => router.push(`/admin/federated-saml/${app.id}/edit`)}
|
||||||
actions={{ newApp: '/admin/federated-saml/new', idpConfiguration: '/.well-known/idp-configuration' }}
|
actions={{
|
||||||
|
newApp: '/admin/federated-saml/new',
|
||||||
|
samlConfiguration: '/.well-known/idp-configuration',
|
||||||
|
oidcConfiguration: '/.well-known/openid-configuration',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { EyeIcon } from '@heroicons/react/24/outline';
|
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
|
||||||
import type { Group } from '../types';
|
import type { Group } from '../types';
|
||||||
import { fetcher, addQueryParamsToPath } from '../utils';
|
import { fetcher, addQueryParamsToPath } from '../utils';
|
||||||
import { DirectoryTab } from '../dsync';
|
import { DirectoryTab } from '../dsync';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { EyeIcon } from '@heroicons/react/24/outline';
|
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
|
||||||
import type { User } from '../types';
|
import type { User } from '../types';
|
||||||
import { addQueryParamsToPath, fetcher } from '../utils';
|
import { addQueryParamsToPath, fetcher } from '../utils';
|
||||||
import { DirectoryTab } from '../dsync';
|
import { DirectoryTab } from '../dsync';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { EyeIcon } from '@heroicons/react/24/outline';
|
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
|
||||||
import type { WebhookEventLog } from '../types';
|
import type { WebhookEventLog } from '../types';
|
||||||
import { fetcher, addQueryParamsToPath } from '../utils';
|
import { fetcher, addQueryParamsToPath } from '../utils';
|
||||||
import { DirectoryTab } from '../dsync';
|
import { DirectoryTab } from '../dsync';
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { useTranslation } from 'next-i18next';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { Card } from '../shared';
|
import { Card } from '../shared';
|
||||||
import { defaultHeaders } from '../utils';
|
import { defaultHeaders } from '../utils';
|
||||||
|
import { ItemList } from '../shared/ItemList';
|
||||||
|
|
||||||
type EditApp = Pick<SAMLFederationApp, 'name' | 'acsUrl' | 'tenants'>;
|
type EditApp = Pick<SAMLFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
|
||||||
|
|
||||||
export const Edit = ({
|
export const Edit = ({
|
||||||
app,
|
app,
|
||||||
|
@ -23,12 +24,16 @@ export const Edit = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const connectionIsOIDC = app.type === 'oidc';
|
||||||
|
const connectionIsSAML = !connectionIsOIDC;
|
||||||
|
|
||||||
const formik = useFormik<EditApp>({
|
const formik = useFormik<EditApp>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: app.name || '',
|
name: app.name || '',
|
||||||
acsUrl: app.acsUrl || '',
|
acsUrl: app.acsUrl || '',
|
||||||
tenants: app.tenants || [],
|
tenants: app.tenants || [],
|
||||||
|
redirectUrl: app.redirectUrl || [],
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const rawResponse = await fetch(urls.patch, {
|
const rawResponse = await fetch(urls.patch, {
|
||||||
|
@ -92,34 +97,74 @@ export const Edit = ({
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<label className='form-control w-full'>
|
{connectionIsOIDC && (
|
||||||
<div className='label'>
|
<label className='form-control w-full'>
|
||||||
<span className='label-text'>{t('bui-fs-acs-url')}</span>
|
<div className='label'>
|
||||||
</div>
|
<span className='label-text'>{t('bui-fs-client-id')}</span>
|
||||||
<input
|
</div>
|
||||||
type='url'
|
<input
|
||||||
placeholder='https://your-sp.com/saml/acs'
|
type='text'
|
||||||
className='input input-bordered w-full text-sm'
|
className='input-bordered input'
|
||||||
name='acsUrl'
|
defaultValue={app.clientID}
|
||||||
value={formik.values.acsUrl}
|
disabled
|
||||||
onChange={formik.handleChange}
|
/>
|
||||||
required
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className='form-control w-full'>
|
|
||||||
<div className='label'>
|
|
||||||
<span className='label-text'>{t('bui-fs-entity-id')}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='input input-bordered w-full text-sm'
|
|
||||||
value={app.entityId}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<label className='label'>
|
|
||||||
<span className='label-text-alt'>{t('bui-fs-entity-id-edit-desc')}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</label>
|
)}
|
||||||
|
{connectionIsOIDC && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-fs-client-secret')}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='input-bordered input'
|
||||||
|
defaultValue={app.clientSecret}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{connectionIsSAML && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-fs-entity-id')}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='input input-bordered w-full text-sm'
|
||||||
|
value={app.entityId}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<label className='label'>
|
||||||
|
<span className='label-text-alt'>{t('bui-fs-entity-id-edit-desc')}</span>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{connectionIsSAML && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-fs-acs-url')}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='url'
|
||||||
|
placeholder='https://your-sp.com/saml/acs'
|
||||||
|
className='input input-bordered w-full text-sm'
|
||||||
|
name='acsUrl'
|
||||||
|
value={formik.values.acsUrl}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{connectionIsOIDC && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-sl-allowed-redirect-urls-new')}</span>
|
||||||
|
</div>
|
||||||
|
<ItemList
|
||||||
|
currentlist={formik.values.redirectUrl || ['']}
|
||||||
|
onItemListChange={(newList) => formik.setFieldValue('redirectUrl', newList)}></ItemList>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label className='form-control w-full'>
|
<label className='form-control w-full'>
|
||||||
<label className='label'>
|
<label className='label'>
|
||||||
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
||||||
|
@ -130,6 +175,7 @@ export const Edit = ({
|
||||||
onlyUnique={true}
|
onlyUnique={true}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
placeholder: t('bui-fs-enter-tenant'),
|
placeholder: t('bui-fs-enter-tenant'),
|
||||||
|
autocomplete: 'off',
|
||||||
}}
|
}}
|
||||||
focusedClassName='input-focused'
|
focusedClassName='input-focused'
|
||||||
addOnBlur={true}
|
addOnBlur={true}
|
||||||
|
|
|
@ -42,6 +42,9 @@ export const EditFederatedSAMLApp = ({
|
||||||
|
|
||||||
const app = data?.data;
|
const app = data?.data;
|
||||||
|
|
||||||
|
const connectionIsOIDC = app.type === 'oidc';
|
||||||
|
const connectionIsSAML = !connectionIsOIDC;
|
||||||
|
|
||||||
const deleteApp = async () => {
|
const deleteApp = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch(urls.deleteApp, { method: 'DELETE', headers: defaultHeaders });
|
await fetch(urls.deleteApp, { method: 'DELETE', headers: defaultHeaders });
|
||||||
|
@ -66,15 +69,17 @@ export const EditFederatedSAMLApp = ({
|
||||||
}}
|
}}
|
||||||
excludeFields={excludeFields}
|
excludeFields={excludeFields}
|
||||||
/>
|
/>
|
||||||
<EditAttributesMapping
|
{connectionIsSAML && (
|
||||||
app={app}
|
<EditAttributesMapping
|
||||||
urls={{ patch: urls.updateApp }}
|
app={app}
|
||||||
onError={onError}
|
urls={{ patch: urls.updateApp }}
|
||||||
onUpdate={(data) => {
|
onError={onError}
|
||||||
mutate({ data });
|
onUpdate={(data) => {
|
||||||
onUpdate?.(data);
|
mutate({ data });
|
||||||
}}
|
onUpdate?.(data);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<EditBranding
|
<EditBranding
|
||||||
app={app}
|
app={app}
|
||||||
urls={{ patch: urls.updateApp }}
|
urls={{ patch: urls.updateApp }}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import type { SAMLFederationApp } from '../types';
|
import type { SAMLFederationApp } from '../types';
|
||||||
import { PencilIcon } from '@heroicons/react/24/outline';
|
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||||
import { TableBodyType } from '../shared/Table';
|
import { TableBodyType } from '../shared/Table';
|
||||||
import { pageLimit } from '../shared/Pagination';
|
import { pageLimit } from '../shared/Pagination';
|
||||||
import { usePaginate } from '../hooks';
|
import { usePaginate } from '../hooks';
|
||||||
|
@ -30,7 +30,7 @@ export const FederatedSAMLApps = ({
|
||||||
urls: { getApps: string };
|
urls: { getApps: string };
|
||||||
excludeFields?: ExcludeFields[];
|
excludeFields?: ExcludeFields[];
|
||||||
onEdit?: (app: SAMLFederationApp) => void;
|
onEdit?: (app: SAMLFederationApp) => void;
|
||||||
actions: { newApp: string; idpConfiguration: string };
|
actions: { newApp: string; samlConfiguration: string; oidcConfiguration: string };
|
||||||
actionCols?: { text: string; onClick: (app: SAMLFederationApp) => void; icon: JSX.Element }[];
|
actionCols?: { text: string; onClick: (app: SAMLFederationApp) => void; icon: JSX.Element }[];
|
||||||
}) => {
|
}) => {
|
||||||
const { router } = useRouter();
|
const { router } = useRouter();
|
||||||
|
@ -128,8 +128,11 @@ export const FederatedSAMLApps = ({
|
||||||
title={t('bui-fs-apps')}
|
title={t('bui-fs-apps')}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<LinkOutline href={actions.idpConfiguration} target='_blank' className='btn-md'>
|
<LinkOutline href={actions.oidcConfiguration} target='_blank' className='btn-md'>
|
||||||
{t('bui-fs-idp-config')}
|
{t('bui-fs-oidc-config')}
|
||||||
|
</LinkOutline>
|
||||||
|
<LinkOutline href={actions.samlConfiguration} target='_blank' className='btn-md'>
|
||||||
|
{t('bui-fs-saml-config')}
|
||||||
</LinkOutline>
|
</LinkOutline>
|
||||||
<ButtonPrimary onClick={() => router?.push(actions.newApp)} className='btn-md'>
|
<ButtonPrimary onClick={() => router?.push(actions.newApp)} className='btn-md'>
|
||||||
{t('bui-fs-new-app')}
|
{t('bui-fs-new-app')}
|
||||||
|
|
|
@ -3,14 +3,15 @@ import TagsInput from 'react-tagsinput';
|
||||||
import { Card, Button } from 'react-daisyui';
|
import { Card, Button } from 'react-daisyui';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import type { SAMLFederationApp } from '../types';
|
import type { SAMLFederationApp } from '../types';
|
||||||
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
|
import QuestionMarkCircleIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon';
|
||||||
import { defaultHeaders } from '../utils';
|
import { defaultHeaders } from '../utils';
|
||||||
import { AttributesMapping } from './AttributesMapping';
|
import { AttributesMapping } from './AttributesMapping';
|
||||||
import { PageHeader } from '../shared';
|
import { PageHeader } from '../shared';
|
||||||
|
import { ItemList } from '../shared/ItemList';
|
||||||
|
|
||||||
type NewSAMLFederationApp = Pick<
|
type NewSAMLFederationApp = Pick<
|
||||||
SAMLFederationApp,
|
SAMLFederationApp,
|
||||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings'
|
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const NewFederatedSAMLApp = ({
|
export const NewFederatedSAMLApp = ({
|
||||||
|
@ -31,6 +32,7 @@ export const NewFederatedSAMLApp = ({
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const initialValues: NewSAMLFederationApp = {
|
const initialValues: NewSAMLFederationApp = {
|
||||||
|
type: 'saml',
|
||||||
name: '',
|
name: '',
|
||||||
tenant: '',
|
tenant: '',
|
||||||
product: '',
|
product: '',
|
||||||
|
@ -65,6 +67,9 @@ export const NewFederatedSAMLApp = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const connectionIsOIDC = formik.values.type === 'oidc';
|
||||||
|
const connectionIsSAML = !connectionIsOIDC;
|
||||||
|
|
||||||
const generateEntityId = () => {
|
const generateEntityId = () => {
|
||||||
const id = crypto.randomUUID().replace(/-/g, '');
|
const id = crypto.randomUUID().replace(/-/g, '');
|
||||||
const entityId = `${samlAudience}/${id}`;
|
const entityId = `${samlAudience}/${id}`;
|
||||||
|
@ -78,6 +83,38 @@ export const NewFederatedSAMLApp = ({
|
||||||
<PageHeader title={t('bui-fs-create-app')} />
|
<PageHeader title={t('bui-fs-create-app')} />
|
||||||
<form onSubmit={formik.handleSubmit} method='POST'>
|
<form onSubmit={formik.handleSubmit} method='POST'>
|
||||||
<Card className='p-6 rounded space-y-3'>
|
<Card className='p-6 rounded space-y-3'>
|
||||||
|
<div className='mb-4 flex items-center'>
|
||||||
|
<div className='mr-2 py-3'>{t('bui-fs-select-app-type')}:</div>
|
||||||
|
<div className='flex w-52'>
|
||||||
|
<div className='form-control'>
|
||||||
|
<label className='label mr-4 cursor-pointer'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='type'
|
||||||
|
value='saml'
|
||||||
|
className='radio-primary radio'
|
||||||
|
checked={formik.values.type === 'saml'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
|
<span className='label-text ml-1'>{t('bui-fs-saml')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='form-control'>
|
||||||
|
<label className='label mr-4 cursor-pointer' data-testid='sso-type-oidc'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='type'
|
||||||
|
value='oidc'
|
||||||
|
className='radio-primary radio'
|
||||||
|
checked={formik.values.type === 'oidc'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
|
<span className='label-text ml-1'>{t('bui-fs-oidc')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label className='form-control w-full'>
|
<label className='form-control w-full'>
|
||||||
<div className='label'>
|
<div className='label'>
|
||||||
<span className='label-text'>{t('bui-shared-name')}</span>
|
<span className='label-text'>{t('bui-shared-name')}</span>
|
||||||
|
@ -98,7 +135,7 @@ export const NewFederatedSAMLApp = ({
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='acme'
|
placeholder='example.com'
|
||||||
className='input input-bordered w-full text-sm'
|
className='input input-bordered w-full text-sm'
|
||||||
name='tenant'
|
name='tenant'
|
||||||
value={formik.values.tenant}
|
value={formik.values.tenant}
|
||||||
|
@ -122,49 +159,63 @@ export const NewFederatedSAMLApp = ({
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<label className='form-control w-full'>
|
{connectionIsSAML && (
|
||||||
<div className='label'>
|
<label className='form-control w-full'>
|
||||||
<span className='label-text'>{t('bui-fs-acs-url')}</span>
|
<div className='label'>
|
||||||
</div>
|
<span className='label-text'>{t('bui-fs-acs-url')}</span>
|
||||||
<input
|
</div>
|
||||||
type='url'
|
<input
|
||||||
placeholder='https://your-sp.com/saml/acs'
|
type='url'
|
||||||
className='input input-bordered w-full text-sm'
|
placeholder='https://your-sp.com/saml/acs'
|
||||||
name='acsUrl'
|
className='input input-bordered w-full text-sm'
|
||||||
value={formik.values.acsUrl}
|
name='acsUrl'
|
||||||
onChange={formik.handleChange}
|
value={formik.values.acsUrl}
|
||||||
required
|
onChange={formik.handleChange}
|
||||||
/>
|
required
|
||||||
</label>
|
/>
|
||||||
<label className='form-control w-full'>
|
|
||||||
<div className='label'>
|
|
||||||
<span className='label-text'>{t('bui-fs-entity-id')}</span>
|
|
||||||
<span className='label-text-alt'>
|
|
||||||
<div className='flex items-center gap-1'>
|
|
||||||
<span
|
|
||||||
className='cursor-pointer border-stone-600 border p-1 rounded'
|
|
||||||
onClick={generateEntityId}>
|
|
||||||
{t('bui-fs-generate-sp-entity-id')}
|
|
||||||
</span>
|
|
||||||
<div className='tooltip tooltip-left' data-tip={t('bui-fs-entity-id-instruction')}>
|
|
||||||
<QuestionMarkCircleIcon className='h-5 w-5' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
placeholder='https://your-sp.com/saml/entityId'
|
|
||||||
className='input input-bordered w-full text-sm'
|
|
||||||
name='entityId'
|
|
||||||
value={formik.values.entityId}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label className='label'>
|
|
||||||
<span className='label-text-alt'>{t('bui-fs-entity-id-change-restriction')}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</label>
|
)}
|
||||||
|
{connectionIsOIDC && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-sl-allowed-redirect-urls-new')}</span>
|
||||||
|
</div>
|
||||||
|
<ItemList
|
||||||
|
currentlist={formik.values.redirectUrl || ['']}
|
||||||
|
onItemListChange={(newList) => formik.setFieldValue('redirectUrl', newList)}></ItemList>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{connectionIsSAML && (
|
||||||
|
<label className='form-control w-full'>
|
||||||
|
<div className='label'>
|
||||||
|
<span className='label-text'>{t('bui-fs-entity-id')}</span>
|
||||||
|
<span className='label-text-alt'>
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
|
<span
|
||||||
|
className='cursor-pointer border-stone-600 border p-1 rounded'
|
||||||
|
onClick={generateEntityId}>
|
||||||
|
{t('bui-fs-generate-sp-entity-id')}
|
||||||
|
</span>
|
||||||
|
<div className='tooltip tooltip-left' data-tip={t('bui-fs-entity-id-instruction')}>
|
||||||
|
<QuestionMarkCircleIcon className='h-5 w-5' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='https://your-sp.com/saml/entityId'
|
||||||
|
className='input input-bordered w-full text-sm'
|
||||||
|
name='entityId'
|
||||||
|
value={formik.values.entityId}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className='label'>
|
||||||
|
<span className='label-text-alt'>{t('bui-fs-entity-id-change-restriction')}</span>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label className='form-control w-full'>
|
<label className='form-control w-full'>
|
||||||
<label className='label'>
|
<label className='label'>
|
||||||
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
||||||
|
@ -175,6 +226,7 @@ export const NewFederatedSAMLApp = ({
|
||||||
onlyUnique={true}
|
onlyUnique={true}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
placeholder: t('bui-fs-enter-tenant'),
|
placeholder: t('bui-fs-enter-tenant'),
|
||||||
|
autocomplete: 'off',
|
||||||
}}
|
}}
|
||||||
focusedClassName='input-focused'
|
focusedClassName='input-focused'
|
||||||
addOnBlur={true}
|
addOnBlur={true}
|
||||||
|
@ -183,20 +235,23 @@ export const NewFederatedSAMLApp = ({
|
||||||
<span className='label-text-alt'>{t('bui-fs-tenants-mapping-desc')}</span>
|
<span className='label-text-alt'>{t('bui-fs-tenants-mapping-desc')}</span>
|
||||||
</label>
|
</label>
|
||||||
</label>
|
</label>
|
||||||
<label className='form-control w-full'>
|
{connectionIsSAML && (
|
||||||
<div className='label'>
|
<label className='form-control w-full'>
|
||||||
<span className='label-text'>{t('bui-fs-attribute-mappings')}</span>
|
<div className='label'>
|
||||||
</div>
|
<span className='label-text'>{t('bui-fs-attribute-mappings')}</span>
|
||||||
<div className='label'>
|
</div>
|
||||||
<span className='label-text-alt'>{t('bui-fs-attribute-mappings-desc')}</span>
|
<div className='label'>
|
||||||
</div>
|
<span className='label-text-alt'>{t('bui-fs-attribute-mappings-desc')}</span>
|
||||||
</label>
|
</div>
|
||||||
<AttributesMapping
|
<AttributesMapping
|
||||||
mappings={formik.values.mappings || []}
|
mappings={formik.values.mappings || []}
|
||||||
onAttributeMappingsChange={(mappings) => formik.setFieldValue('mappings', mappings)}
|
onAttributeMappingsChange={(mappings) => formik.setFieldValue('mappings', mappings)}
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<div className='flex gap-2 justify-end pt-6'>
|
<div className='flex gap-2 justify-end pt-6'>
|
||||||
<Button
|
<Button
|
||||||
|
type='submit'
|
||||||
className='btn btn-primary btn-md'
|
className='btn btn-primary btn-md'
|
||||||
loading={formik.isSubmitting}
|
loading={formik.isSubmitting}
|
||||||
disabled={!formik.dirty || !formik.isValid}>
|
disabled={!formik.dirty || !formik.isValid}>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
||||||
|
|
||||||
|
export const ItemList = ({
|
||||||
|
currentlist,
|
||||||
|
onItemListChange,
|
||||||
|
}: {
|
||||||
|
currentlist: string | string[];
|
||||||
|
onItemListChange: (list: string[]) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const list = Array.isArray(currentlist) ? currentlist : [currentlist];
|
||||||
|
|
||||||
|
const addAnother = () => {
|
||||||
|
onItemListChange([...list, '']);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<ItemRow
|
||||||
|
item={item}
|
||||||
|
onItemChange={(newItem) => {
|
||||||
|
const newList = [...list];
|
||||||
|
newList[index] = newItem;
|
||||||
|
onItemListChange(newList);
|
||||||
|
}}
|
||||||
|
onItemDelete={() => {
|
||||||
|
onItemListChange(list.filter((_, i) => i !== index));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<button className='btn btn-primary btn-sm btn-outline' type='button' onClick={addAnother}>
|
||||||
|
{t('bui-fs-add')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemRow = ({
|
||||||
|
item,
|
||||||
|
onItemChange,
|
||||||
|
onItemDelete,
|
||||||
|
}: {
|
||||||
|
item: string;
|
||||||
|
onItemChange: (newItem: string) => void;
|
||||||
|
onItemDelete: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='flex space-x-3 items-center'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='input input-bordered input-sm w-full'
|
||||||
|
name='item'
|
||||||
|
value={item}
|
||||||
|
onChange={(e) => {
|
||||||
|
onItemChange(e.target.value);
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type='button' onClick={onItemDelete}>
|
||||||
|
<XMarkIcon className='h-5 w-5 text-red-500' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -91,6 +91,10 @@ export type AttributeMapping = {
|
||||||
|
|
||||||
export type SAMLFederationApp = {
|
export type SAMLFederationApp = {
|
||||||
id: string;
|
id: string;
|
||||||
|
type?: string;
|
||||||
|
clientID?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
redirectUrl?: string[] | string;
|
||||||
name: string;
|
name: string;
|
||||||
tenant: string;
|
tenant: string;
|
||||||
product: string;
|
product: string;
|
||||||
|
|
|
@ -48,19 +48,26 @@ export const WellKnownURLs = ({ jacksonUrl }: { jacksonUrl?: string }) => {
|
||||||
type: 'auth',
|
type: 'auth',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('bui-wku-idp-metadata'),
|
title: t('bui-wku-saml-idp-metadata'),
|
||||||
description: t('bui-wku-idp-metadata-desc'),
|
description: t('bui-wku-saml-idp-metadata-desc'),
|
||||||
href: `${baseUrl}/.well-known/idp-metadata`,
|
href: `${baseUrl}/.well-known/idp-metadata`,
|
||||||
buttonText: viewText,
|
buttonText: viewText,
|
||||||
type: 'saml-fed',
|
type: 'saml-fed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('bui-wku-idp-configuration'),
|
title: t('bui-wku-saml-idp-configuration'),
|
||||||
description: t('bui-wku-idp-config-desc'),
|
description: t('bui-wku-saml-idp-config-desc'),
|
||||||
href: `${baseUrl}/.well-known/idp-configuration`,
|
href: `${baseUrl}/.well-known/idp-configuration`,
|
||||||
buttonText: viewText,
|
buttonText: viewText,
|
||||||
type: 'saml-fed',
|
type: 'saml-fed',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('bui-wku-oidc-federation'),
|
||||||
|
description: t('bui-wku-oidc-federation-desc'),
|
||||||
|
href: `${baseUrl}/.well-known/openid-configuration`,
|
||||||
|
buttonText: viewText,
|
||||||
|
type: 'saml-fed',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -55,10 +55,10 @@
|
||||||
"webhook_secret": "Webhook secret",
|
"webhook_secret": "Webhook secret",
|
||||||
"webhook_url": "Webhook URL",
|
"webhook_url": "Webhook URL",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"saml_federation_new_success": "SAML Federation app created successfully.",
|
"saml_federation_new_success": "Identity Federation app created successfully.",
|
||||||
"entity_id": "Entity ID / Audience URI / Audience Restriction",
|
"entity_id": "Entity ID / Audience URI / Audience Restriction",
|
||||||
"saml_federation_update_success": "SAML Federation app updated successfully.",
|
"saml_federation_update_success": "Identity Federation app updated successfully.",
|
||||||
"saml_federation_delete_success": "SAML federation app deleted successfully",
|
"saml_federation_delete_success": "Identity federation app deleted successfully",
|
||||||
"saml_federation_app_info": "SAML Federation App Information",
|
"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",
|
"saml_federation_app_info_details": "Choose from the following options to configure your SAML Federation on the service provider side",
|
||||||
"download_metadata": "Download Metadata",
|
"download_metadata": "Download Metadata",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"directory_created_successfully": "Directory created successfully",
|
"directory_created_successfully": "Directory created successfully",
|
||||||
"directory_updated_successfully": "Directory updated successfully",
|
"directory_updated_successfully": "Directory updated successfully",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"saml_federation": "SAML Federation",
|
"saml_federation": "Identity Federation",
|
||||||
"sso_tracer": "SSO Tracer",
|
"sso_tracer": "SSO Tracer",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"admin_portal_sso": "SSO for Admin Portal",
|
"admin_portal_sso": "SSO for Admin Portal",
|
||||||
|
@ -194,12 +194,19 @@
|
||||||
"bui-wku-oidc-config-desc": "URIs that your customers will need to set up the OIDC app on the Identity Provider.",
|
"bui-wku-oidc-config-desc": "URIs that your customers will need to set up the OIDC app on the Identity Provider.",
|
||||||
"bui-wku-oidc-discovery": "OpenID Connect Discovery",
|
"bui-wku-oidc-discovery": "OpenID Connect Discovery",
|
||||||
"bui-wku-oidc-discovery-desc": "Our OpenID well known URI which your customers will need if they are authenticating via OAuth 2.0 or Open ID Connect.",
|
"bui-wku-oidc-discovery-desc": "Our OpenID well known URI which your customers will need if they are authenticating via OAuth 2.0 or Open ID Connect.",
|
||||||
"bui-wku-idp-metadata": "IdP Metadata",
|
"bui-wku-saml-idp-metadata": "SAML IdP Metadata",
|
||||||
"bui-wku-idp-metadata-desc": "The metadata file that your customers who use our SAML federation feature will need to set up SAML SP configuration on their application.",
|
"bui-wku-saml-idp-metadata-desc": "The metadata file that your customers who use our SAML federation feature will need to set up SAML SP configuration on their application.",
|
||||||
"bui-wku-idp-configuration": "IdP Configuration",
|
"bui-wku-saml-idp-configuration": "SAML Federation IdP Configuration",
|
||||||
"bui-wku-idp-config-desc": "The configuration setup guide that your customers who use our SAML federation feature will need to set up SAML SP configuration on their application.",
|
"bui-wku-saml-idp-config-desc": "The configuration setup guide that your customers who use our SAML federation feature will need to set up SAML SP configuration on their application.",
|
||||||
|
"bui-wku-oidc-federation": "OIDC Federation",
|
||||||
|
"bui-wku-oidc-federation-desc": "Our OIDC Federation well known URI which you will need if are configuring Identity Federation via OpenID Connect.",
|
||||||
"bui-wku-view": "View",
|
"bui-wku-view": "View",
|
||||||
"bui-wku-download": "Download",
|
"bui-wku-download": "Download",
|
||||||
|
"bui-fs-client-id": "Client ID",
|
||||||
|
"bui-fs-client-secret": "Client Secret",
|
||||||
|
"bui-fs-saml": "SAML",
|
||||||
|
"bui-fs-oidc": "OIDC",
|
||||||
|
"bui-fs-select-app-type": "Select App Type",
|
||||||
"bui-fs-generate-sp-entity-id": "Generate Entity ID",
|
"bui-fs-generate-sp-entity-id": "Generate Entity ID",
|
||||||
"bui-fs-entity-id-instruction": "Use the button to create a distinctive identifier if your service provider does not provide a unique Entity ID and set that in your provider's configuration. If your service provider does provide a unique ID, you can use that instead.",
|
"bui-fs-entity-id-instruction": "Use the button to create a distinctive identifier if your service provider does not provide a unique Entity ID and set that in your provider's configuration. If your service provider does provide a unique ID, you can use that instead.",
|
||||||
"bui-fs-entity-id-change-restriction": "You can't change this value once the app is created.",
|
"bui-fs-entity-id-change-restriction": "You can't change this value once the app is created.",
|
||||||
|
@ -208,15 +215,16 @@
|
||||||
"bui-fs-tenants-mapping-desc": "Provide a custom mapping for SAML federation apps that lets you associate multiple tenants to it. Enter the tenant name and press ENTER or TAB.",
|
"bui-fs-tenants-mapping-desc": "Provide a custom mapping for SAML federation apps that lets you associate multiple tenants to it. Enter the tenant name and press ENTER or TAB.",
|
||||||
"bui-fs-attribute-mappings": "Attribute Mappings",
|
"bui-fs-attribute-mappings": "Attribute Mappings",
|
||||||
"bui-fs-attribute-mappings-desc": "Map the attributes from your IdP to your SP (if needed)",
|
"bui-fs-attribute-mappings-desc": "Map the attributes from your IdP to your SP (if needed)",
|
||||||
"bui-fs-create-app": "Create SAML Federation App",
|
"bui-fs-create-app": "Create Identity Federation App",
|
||||||
"bui-fs-create-app-btn": "Create App",
|
"bui-fs-create-app-btn": "Create App",
|
||||||
"bui-fs-edit-app": "Edit SAML Federation App",
|
"bui-fs-edit-app": "Edit Identity Federation App",
|
||||||
"bui-fs-sp-attribute": "SP Attribute",
|
"bui-fs-sp-attribute": "SP Attribute",
|
||||||
"bui-fs-idp-attribute": "IdP Attribute",
|
"bui-fs-idp-attribute": "IdP Attribute",
|
||||||
"bui-fs-add-mapping": "Add Mapping",
|
"bui-fs-add-mapping": "Add Mapping",
|
||||||
"bui-fs-add-another": "Add another",
|
"bui-fs-add-another": "Add another",
|
||||||
"bui-fs-no-apps": "No SAML Federation Apps found.",
|
"bui-fs-add": "Add",
|
||||||
"bui-fs-no-apps-desc": "Create a new SAML Federation App to configure SAML Federation.",
|
"bui-fs-no-apps": "No Identity Federation Apps found.",
|
||||||
|
"bui-fs-no-apps-desc": "Create a new App to configure Identity Federation.",
|
||||||
"bui-fs-acs-url": "ACS URL",
|
"bui-fs-acs-url": "ACS URL",
|
||||||
"bui-fs-entity-id": "Entity ID / Audience URI / Audience Restriction",
|
"bui-fs-entity-id": "Entity ID / Audience URI / Audience Restriction",
|
||||||
"bui-fs-branding-title": "Customize Look and Feel",
|
"bui-fs-branding-title": "Customize Look and Feel",
|
||||||
|
@ -228,11 +236,12 @@
|
||||||
"bui-fs-primary-color": "Primary Color",
|
"bui-fs-primary-color": "Primary Color",
|
||||||
"bui-fs-primary-color-desc": "Primary color will be applied to buttons, links, and other elements.",
|
"bui-fs-primary-color-desc": "Primary color will be applied to buttons, links, and other elements.",
|
||||||
"bui-fs-entity-id-edit-desc": "You can't change this value. Delete and create a new app if you need to change it.",
|
"bui-fs-entity-id-edit-desc": "You can't change this value. Delete and create a new app if you need to change it.",
|
||||||
"bui-fs-delete-app-title": "Delete this SAML Federation app",
|
"bui-fs-delete-app-title": "Delete this Identity Federation app",
|
||||||
"bui-fs-delete-app-desc": "This action cannot be undone. This will permanently delete the SAML Federation app.",
|
"bui-fs-delete-app-desc": "This action cannot be undone. This will permanently delete the Identity Federation app.",
|
||||||
"bui-fs-apps": "SAML Federation Apps",
|
"bui-fs-apps": "Identity Federation Apps",
|
||||||
"bui-fs-new-app": "New App",
|
"bui-fs-new-app": "New App",
|
||||||
"bui-fs-idp-config": "IdP Configuration",
|
"bui-fs-saml-config": "SAML Configuration",
|
||||||
|
"bui-fs-oidc-config": "OIDC Configuration",
|
||||||
"bui-fs-saml-attributes": "SAML Attributes",
|
"bui-fs-saml-attributes": "SAML Attributes",
|
||||||
"bui-fs-oidc-attributes": "OIDC Attributes",
|
"bui-fs-oidc-attributes": "OIDC Attributes",
|
||||||
"bui-dsync-name": "Name",
|
"bui-dsync-name": "Name",
|
||||||
|
@ -313,6 +322,7 @@
|
||||||
"bui-sl-expiry-days": "Expiry in days",
|
"bui-sl-expiry-days": "Expiry in days",
|
||||||
"bui-sl-default-redirect-url": "Default redirect URL",
|
"bui-sl-default-redirect-url": "Default redirect URL",
|
||||||
"bui-sl-allowed-redirect-urls": "Allowed redirect URLs (newline separated)",
|
"bui-sl-allowed-redirect-urls": "Allowed redirect URLs (newline separated)",
|
||||||
|
"bui-sl-allowed-redirect-urls-new": "Allowed redirect URLs",
|
||||||
"bui-sl-sso-desc": "Create a unique Setup Link to share with your customers so they can set Enterprise SSO connection with your app.",
|
"bui-sl-sso-desc": "Create a unique Setup Link to share with your customers so they can set Enterprise SSO connection with your app.",
|
||||||
"bui-sl-dsync-desc": "Create a unique Setup Link to share with your customers so they can set Directory Sync connection with your app.",
|
"bui-sl-dsync-desc": "Create a unique Setup Link to share with your customers so they can set Directory Sync connection with your app.",
|
||||||
"bui-sl-dsync-name-placeholder": "Acme Directory",
|
"bui-sl-dsync-name-placeholder": "Acme Directory",
|
||||||
|
|
|
@ -3176,8 +3176,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.24",
|
"version": "20.11.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
|
"license": "MIT",
|
||||||
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
|
@ -4809,6 +4808,18 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gcp-metadata": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^6.0.0",
|
||||||
|
"json-bigint": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/generate-function": {
|
"node_modules/generate-function": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
|
@ -4930,18 +4941,6 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/google-auth-library/node_modules/gcp-metadata": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
|
|
||||||
"dependencies": {
|
|
||||||
"gaxios": "^6.0.0",
|
|
||||||
"json-bigint": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/googleapis-common": {
|
"node_modules/googleapis-common": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz",
|
||||||
|
@ -6327,8 +6326,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/mongodb": {
|
"node_modules/mongodb": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.4.0.tgz",
|
"license": "Apache-2.0",
|
||||||
"integrity": "sha512-MdFHsyb1a/Ee0H3NmzWTSLqchacDV/APF0H6BNQvraWrOiIocys2EmTFZPgHxWhcfO94c1F34I9MACU7x0hHKA==",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mongodb-js/saslprep": "^1.1.0",
|
"@mongodb-js/saslprep": "^1.1.0",
|
||||||
"bson": "^6.4.0",
|
"bson": "^6.4.0",
|
||||||
|
@ -7734,11 +7732,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||||
"integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
|
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.6",
|
"call-bind": "^1.0.7",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"get-intrinsic": "^1.2.4",
|
"get-intrinsic": "^1.2.4",
|
||||||
"object-inspect": "^1.13.1"
|
"object-inspect": "^1.13.1"
|
||||||
|
@ -8882,9 +8880,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
|
"node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.23",
|
"version": "0.3.24",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz",
|
||||||
"integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
|
"integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { deflateRaw } from 'zlib';
|
||||||
import saml from '@boxyhq/saml20';
|
import saml from '@boxyhq/saml20';
|
||||||
import { TokenSet, errors, generators } from 'openid-client';
|
import { TokenSet, errors, generators } from 'openid-client';
|
||||||
import { SAMLProfile } from '@boxyhq/saml20/dist/typings';
|
import { SAMLProfile } from '@boxyhq/saml20/dist/typings';
|
||||||
|
import { clientIDFederatedPrefix, clientIDOIDCPrefix } from './utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IOAuthController,
|
IOAuthController,
|
||||||
|
@ -20,6 +21,7 @@ import type {
|
||||||
SSOTracerInstance,
|
SSOTracerInstance,
|
||||||
OAuthErrorHandlerParams,
|
OAuthErrorHandlerParams,
|
||||||
OIDCAuthzResponsePayload,
|
OIDCAuthzResponsePayload,
|
||||||
|
SAMLFederationApp,
|
||||||
} from '../typings';
|
} from '../typings';
|
||||||
import {
|
import {
|
||||||
relayStatePrefix,
|
relayStatePrefix,
|
||||||
|
@ -43,6 +45,7 @@ import { getDefaultCertificate } from '../saml/x509';
|
||||||
import { SSOHandler } from './sso-handler';
|
import { SSOHandler } from './sso-handler';
|
||||||
import { ValidateOption, extractSAMLResponseAttributes } from '../saml/lib';
|
import { ValidateOption, extractSAMLResponseAttributes } from '../saml/lib';
|
||||||
import { oidcIssuerInstance } from './oauth/oidc-issuer';
|
import { oidcIssuerInstance } from './oauth/oidc-issuer';
|
||||||
|
import { App } from '../ee/federated-saml/app';
|
||||||
|
|
||||||
const deflateRawAsync = promisify(deflateRaw);
|
const deflateRawAsync = promisify(deflateRaw);
|
||||||
|
|
||||||
|
@ -54,14 +57,16 @@ export class OAuthController implements IOAuthController {
|
||||||
private ssoTracer: SSOTracerInstance;
|
private ssoTracer: SSOTracerInstance;
|
||||||
private opts: JacksonOption;
|
private opts: JacksonOption;
|
||||||
private ssoHandler: SSOHandler;
|
private ssoHandler: SSOHandler;
|
||||||
|
private samlFedApp: App;
|
||||||
|
|
||||||
constructor({ connectionStore, sessionStore, codeStore, tokenStore, ssoTracer, opts }) {
|
constructor({ connectionStore, sessionStore, codeStore, tokenStore, ssoTracer, opts, samlFedApp }) {
|
||||||
this.connectionStore = connectionStore;
|
this.connectionStore = connectionStore;
|
||||||
this.sessionStore = sessionStore;
|
this.sessionStore = sessionStore;
|
||||||
this.codeStore = codeStore;
|
this.codeStore = codeStore;
|
||||||
this.tokenStore = tokenStore;
|
this.tokenStore = tokenStore;
|
||||||
this.ssoTracer = ssoTracer;
|
this.ssoTracer = ssoTracer;
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
|
this.samlFedApp = samlFedApp;
|
||||||
|
|
||||||
this.ssoHandler = new SSOHandler({
|
this.ssoHandler = new SSOHandler({
|
||||||
connection: connectionStore,
|
connection: connectionStore,
|
||||||
|
@ -90,6 +95,7 @@ export class OAuthController implements IOAuthController {
|
||||||
let requestedScopes: string[] | undefined;
|
let requestedScopes: string[] | undefined;
|
||||||
let requestedOIDCFlow: boolean | undefined;
|
let requestedOIDCFlow: boolean | undefined;
|
||||||
let connection: SAMLSSORecord | OIDCSSORecord | undefined;
|
let connection: SAMLSSORecord | OIDCSSORecord | undefined;
|
||||||
|
let fedApp: SAMLFederationApp | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tenant = 'tenant' in body ? body.tenant : undefined;
|
const tenant = 'tenant' in body ? body.tenant : undefined;
|
||||||
|
@ -167,10 +173,40 @@ export class OAuthController implements IOAuthController {
|
||||||
connection = response.connection;
|
connection = response.connection;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
connection = await this.connectionStore.get(client_id);
|
// client_id is not encoded, so we look for the connection using the client_id
|
||||||
if (connection) {
|
// First we check if it's a federated connection
|
||||||
requestedTenant = connection.tenant;
|
if (client_id.startsWith(`${clientIDFederatedPrefix}${clientIDOIDCPrefix}`)) {
|
||||||
requestedProduct = connection.product;
|
fedApp = await this.samlFedApp.get({
|
||||||
|
id: client_id.replace(clientIDFederatedPrefix, ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.ssoHandler.resolveConnection({
|
||||||
|
tenant: fedApp.tenant,
|
||||||
|
product: fedApp.product,
|
||||||
|
idp_hint,
|
||||||
|
authFlow: 'oauth',
|
||||||
|
originalParams: { ...body },
|
||||||
|
tenants: fedApp.tenants,
|
||||||
|
samlFedAppId: fedApp.id,
|
||||||
|
fedType: fedApp.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('redirectUrl' in response) {
|
||||||
|
return {
|
||||||
|
redirect_url: response.redirectUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('connection' in response) {
|
||||||
|
connection = response.connection;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If it's not a federated connection, we look for the connection using the client_id
|
||||||
|
connection = await this.connectionStore.get(client_id);
|
||||||
|
if (connection) {
|
||||||
|
requestedTenant = connection.tenant;
|
||||||
|
requestedProduct = connection.product;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -182,7 +218,13 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowed.redirect(redirect_uri, connection.redirectUrl as string[])) {
|
if (!allowed.redirect(redirect_uri, connection.redirectUrl as string[])) {
|
||||||
throw new JacksonError('Redirect URL is not allowed.', 403);
|
if (fedApp) {
|
||||||
|
if (!allowed.redirect(redirect_uri, fedApp.redirectUrl as string[])) {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error_description = getErrorMessage(err);
|
const error_description = getErrorMessage(err);
|
||||||
|
@ -399,6 +441,10 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
if (idp_hint) {
|
if (idp_hint) {
|
||||||
requested.idp_hint = idp_hint;
|
requested.idp_hint = idp_hint;
|
||||||
|
} else {
|
||||||
|
if (fedApp) {
|
||||||
|
requested.idp_hint = connection.clientID;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (requestedOIDCFlow) {
|
if (requestedOIDCFlow) {
|
||||||
requested.oidc = true;
|
requested.oidc = true;
|
||||||
|
@ -417,6 +463,7 @@ export class OAuthController implements IOAuthController {
|
||||||
code_challenge,
|
code_challenge,
|
||||||
code_challenge_method,
|
code_challenge_method,
|
||||||
requested,
|
requested,
|
||||||
|
oidcFederated: fedApp ? { redirectUrl: fedApp.redirectUrl, id: fedApp.id } : undefined,
|
||||||
};
|
};
|
||||||
await this.sessionStore.put(
|
await this.sessionStore.put(
|
||||||
sessionId,
|
sessionId,
|
||||||
|
@ -495,6 +542,7 @@ export class OAuthController implements IOAuthController {
|
||||||
let issuer: string | undefined;
|
let issuer: string | undefined;
|
||||||
let isIdPFlow: boolean | undefined;
|
let isIdPFlow: boolean | undefined;
|
||||||
let isSAMLFederated: boolean | undefined;
|
let isSAMLFederated: boolean | undefined;
|
||||||
|
let isOIDCFederated: boolean | undefined;
|
||||||
let validateOpts: ValidateOption;
|
let validateOpts: ValidateOption;
|
||||||
let redirect_uri: string | undefined;
|
let redirect_uri: string | undefined;
|
||||||
const { SAMLResponse, idp_hint, RelayState = '' } = body;
|
const { SAMLResponse, idp_hint, RelayState = '' } = body;
|
||||||
|
@ -536,6 +584,7 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
isSAMLFederated = session && 'samlFederated' in session;
|
isSAMLFederated = session && 'samlFederated' in session;
|
||||||
|
isOIDCFederated = session && 'oidcFederated' in session;
|
||||||
const isSPFlow = !isIdPFlow && !isSAMLFederated;
|
const isSPFlow = !isIdPFlow && !isSAMLFederated;
|
||||||
|
|
||||||
// IdP initiated SSO flow
|
// IdP initiated SSO flow
|
||||||
|
@ -564,10 +613,11 @@ export class OAuthController implements IOAuthController {
|
||||||
|
|
||||||
// SP initiated SSO flow
|
// SP initiated SSO flow
|
||||||
// Resolve if there are multiple matches for SP login
|
// Resolve if there are multiple matches for SP login
|
||||||
if (isSPFlow || isSAMLFederated) {
|
if (isSPFlow || isSAMLFederated || isOIDCFederated) {
|
||||||
connection = connections.filter((c) => {
|
connection = connections.filter((c) => {
|
||||||
return (
|
return (
|
||||||
c.clientID === session.requested.client_id ||
|
c.clientID === session.requested.client_id ||
|
||||||
|
c.clientID === session.requested.idp_hint ||
|
||||||
(c.tenant === session.requested.tenant && c.product === session.requested.product)
|
(c.tenant === session.requested.tenant && c.product === session.requested.product)
|
||||||
);
|
);
|
||||||
})[0];
|
})[0];
|
||||||
|
@ -582,7 +632,13 @@ export class OAuthController implements IOAuthController {
|
||||||
session.redirect_uri &&
|
session.redirect_uri &&
|
||||||
!allowed.redirect(session.redirect_uri, connection.redirectUrl as string[])
|
!allowed.redirect(session.redirect_uri, connection.redirectUrl as string[])
|
||||||
) {
|
) {
|
||||||
throw new JacksonError('Redirect URL is not allowed.', 403);
|
if (isOIDCFederated) {
|
||||||
|
if (!allowed.redirect(session.redirect_uri, session.oidcFederated?.redirectUrl as string[])) {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { privateKey } = await getDefaultCertificate();
|
const { privateKey } = await getDefaultCertificate();
|
||||||
|
@ -696,6 +752,7 @@ export class OAuthController implements IOAuthController {
|
||||||
let oidcConnection: OIDCSSORecord | undefined;
|
let oidcConnection: OIDCSSORecord | undefined;
|
||||||
let session: any;
|
let session: any;
|
||||||
let isSAMLFederated: boolean | undefined;
|
let isSAMLFederated: boolean | undefined;
|
||||||
|
let isOIDCFederated: boolean | undefined;
|
||||||
let redirect_uri: string | undefined;
|
let redirect_uri: string | undefined;
|
||||||
let profile;
|
let profile;
|
||||||
|
|
||||||
|
@ -714,6 +771,7 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
isSAMLFederated = session && 'samlFederated' in session;
|
isSAMLFederated = session && 'samlFederated' in session;
|
||||||
|
isOIDCFederated = session && 'oidcFederated' in session;
|
||||||
|
|
||||||
oidcConnection = await this.connectionStore.get(session.id);
|
oidcConnection = await this.connectionStore.get(session.id);
|
||||||
|
|
||||||
|
@ -728,7 +786,13 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirect_uri && !allowed.redirect(redirect_uri, oidcConnection.redirectUrl as string[])) {
|
if (redirect_uri && !allowed.redirect(redirect_uri, oidcConnection.redirectUrl as string[])) {
|
||||||
throw new JacksonError('Redirect URL is not allowed.', 403);
|
if (isOIDCFederated) {
|
||||||
|
if (!allowed.redirect(redirect_uri, session.oidcFederated?.redirectUrl as string[])) {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new JacksonError('Redirect URL is not allowed.', 403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -744,6 +808,7 @@ export class OAuthController implements IOAuthController {
|
||||||
redirectUri: redirect_uri,
|
redirectUri: redirect_uri,
|
||||||
relayState: RelayState,
|
relayState: RelayState,
|
||||||
isSAMLFederated: !!isSAMLFederated,
|
isSAMLFederated: !!isSAMLFederated,
|
||||||
|
isOIDCFederated: !!isOIDCFederated,
|
||||||
requestedOIDCFlow: !!session?.requested?.oidc,
|
requestedOIDCFlow: !!session?.requested?.oidc,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -803,6 +868,7 @@ export class OAuthController implements IOAuthController {
|
||||||
redirectUri: redirect_uri,
|
redirectUri: redirect_uri,
|
||||||
relayState: RelayState,
|
relayState: RelayState,
|
||||||
isSAMLFederated: !!isSAMLFederated,
|
isSAMLFederated: !!isSAMLFederated,
|
||||||
|
isOIDCFederated: !!isOIDCFederated,
|
||||||
acsUrl: session.requested.acsUrl,
|
acsUrl: session.requested.acsUrl,
|
||||||
entityId: session.requested.entityId,
|
entityId: session.requested.entityId,
|
||||||
requestedOIDCFlow: !!session.requested.oidc,
|
requestedOIDCFlow: !!session.requested.oidc,
|
||||||
|
|
|
@ -11,9 +11,22 @@ export const redirect = (redirectUrl: string, redirectUrls: string[]): boolean =
|
||||||
for (const idx in redirectUrls) {
|
for (const idx in redirectUrls) {
|
||||||
const rUrl: URL = new URL(redirectUrls[idx]);
|
const rUrl: URL = new URL(redirectUrls[idx]);
|
||||||
|
|
||||||
|
let hostname = url.hostname;
|
||||||
|
let hostNameAllowed = rUrl.hostname;
|
||||||
|
|
||||||
|
// allow subdomain globbing *.example.com only
|
||||||
|
try {
|
||||||
|
if (rUrl.hostname.startsWith('*.')) {
|
||||||
|
hostNameAllowed = rUrl.hostname.slice(2);
|
||||||
|
hostname = hostname.slice(hostname.indexOf('.') + 1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Check pathname, for now pathname is ignored
|
// TODO: Check pathname, for now pathname is ignored
|
||||||
|
|
||||||
if (rUrl.protocol === url.protocol && rUrl.hostname === url.hostname && rUrl.port === url.port) {
|
if (rUrl.protocol === url.protocol && hostNameAllowed === hostname && rUrl.port === url.port) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ export class SSOHandler {
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
idp_hint?: string;
|
idp_hint?: string;
|
||||||
samlFedAppId?: string;
|
samlFedAppId?: string;
|
||||||
|
fedType?: string;
|
||||||
tenants?: string[]; // Only used for SAML IdP initiated flow
|
tenants?: string[]; // Only used for SAML IdP initiated flow
|
||||||
}): Promise<
|
}): Promise<
|
||||||
| {
|
| {
|
||||||
|
@ -67,9 +68,22 @@ export class SSOHandler {
|
||||||
entityId,
|
entityId,
|
||||||
tenants,
|
tenants,
|
||||||
samlFedAppId = '',
|
samlFedAppId = '',
|
||||||
|
fedType = '',
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let connections: (SAMLSSORecord | OIDCSSORecord)[] | null = null;
|
let connections: (SAMLSSORecord | OIDCSSORecord)[] | null = null;
|
||||||
|
const noSSOConnectionErrMessage = 'No SSO connection found.';
|
||||||
|
|
||||||
|
// If an IdP is specified, find the connection for that IdP
|
||||||
|
if (idp_hint) {
|
||||||
|
const connection = await this.connection.get(idp_hint);
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
|
throw new JacksonError(noSSOConnectionErrMessage, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connection };
|
||||||
|
}
|
||||||
|
|
||||||
// Find SAML connections for the app
|
// Find SAML connections for the app
|
||||||
if (tenants && tenants.length > 0 && product) {
|
if (tenants && tenants.length > 0 && product) {
|
||||||
|
@ -99,36 +113,27 @@ export class SSOHandler {
|
||||||
connections = result.data;
|
connections = result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noSSOConnectionErrMessage = 'No SSO connection found.';
|
|
||||||
|
|
||||||
if (!connections || connections.length === 0) {
|
if (!connections || connections.length === 0) {
|
||||||
throw new JacksonError(noSSOConnectionErrMessage, 404);
|
throw new JacksonError(noSSOConnectionErrMessage, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an IdP is specified, find the connection for that IdP
|
|
||||||
if (idp_hint) {
|
|
||||||
const connection = connections.find((c) => c.clientID === idp_hint);
|
|
||||||
|
|
||||||
if (!connection) {
|
|
||||||
throw new JacksonError(noSSOConnectionErrMessage, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { connection };
|
|
||||||
}
|
|
||||||
|
|
||||||
// If more than one, redirect to the connection selection page
|
// If more than one, redirect to the connection selection page
|
||||||
if (connections.length > 1) {
|
if (connections.length > 1) {
|
||||||
const url = new URL(`${this.opts.externalUrl}${this.opts.idpDiscoveryPath}`);
|
const url = new URL(`${this.opts.externalUrl}${this.opts.idpDiscoveryPath}`);
|
||||||
|
|
||||||
// SP initiated flow
|
// SP initiated flow
|
||||||
if (['oauth', 'saml'].includes(authFlow) && tenant && product) {
|
if (['oauth', 'saml'].includes(authFlow)) {
|
||||||
const params = new URLSearchParams({
|
const qps = {
|
||||||
tenant,
|
|
||||||
product,
|
|
||||||
authFlow: 'sp-initiated',
|
authFlow: 'sp-initiated',
|
||||||
samlFedAppId,
|
samlFedAppId,
|
||||||
|
fedType,
|
||||||
...originalParams,
|
...originalParams,
|
||||||
});
|
};
|
||||||
|
if (tenant && product && fedType !== 'oidc') {
|
||||||
|
qps['tenant'] = tenant;
|
||||||
|
qps['product'] = product;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams(qps);
|
||||||
|
|
||||||
return { redirectUrl: `${url}?${params}` };
|
return { redirectUrl: `${url}?${params}` };
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,8 @@ export const storeNamespacePrefix = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relayStatePrefix = 'boxyhq_jackson_';
|
export const relayStatePrefix = 'boxyhq_jackson_';
|
||||||
|
export const clientIDFederatedPrefix = 'fed_';
|
||||||
|
export const clientIDOIDCPrefix = 'oidc_';
|
||||||
|
|
||||||
export const validateAbsoluteUrl = (url, message) => {
|
export const validateAbsoluteUrl = (url, message) => {
|
||||||
try {
|
try {
|
||||||
|
@ -302,6 +304,10 @@ export const appID = (tenant: string, product: string) => {
|
||||||
return dbutils.keyDigest(dbutils.keyFromParts(tenant, product));
|
return dbutils.keyDigest(dbutils.keyFromParts(tenant, product));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fedAppID = (tenant: string, product: string, type?: string) => {
|
||||||
|
return (type === 'oidc' ? clientIDOIDCPrefix : '') + appID(tenant, product);
|
||||||
|
};
|
||||||
|
|
||||||
// List of well known providers
|
// List of well known providers
|
||||||
const wellKnownProviders = {
|
const wellKnownProviders = {
|
||||||
'okta.com': 'Okta',
|
'okta.com': 'Okta',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
import type {
|
import type {
|
||||||
Storable,
|
Storable,
|
||||||
JacksonOption,
|
JacksonOption,
|
||||||
|
@ -6,7 +7,7 @@ import type {
|
||||||
GetByProductParams,
|
GetByProductParams,
|
||||||
AppRequestParams,
|
AppRequestParams,
|
||||||
} from '../../typings';
|
} from '../../typings';
|
||||||
import { appID } from '../../controller/utils';
|
import { fedAppID, clientIDFederatedPrefix } from '../../controller/utils';
|
||||||
import { createMetadataXML } from '../../saml/lib';
|
import { createMetadataXML } from '../../saml/lib';
|
||||||
import { JacksonError } from '../../controller/error';
|
import { JacksonError } from '../../controller/error';
|
||||||
import { getDefaultCertificate } from '../../saml/x509';
|
import { getDefaultCertificate } from '../../saml/x509';
|
||||||
|
@ -15,7 +16,7 @@ import { throwIfInvalidLicense } from '../common/checkLicense';
|
||||||
|
|
||||||
type NewAppParams = Pick<
|
type NewAppParams = Pick<
|
||||||
SAMLFederationApp,
|
SAMLFederationApp,
|
||||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings'
|
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||||
> & {
|
> & {
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
faviconUrl?: string;
|
faviconUrl?: string;
|
||||||
|
@ -70,7 +71,7 @@ export class App {
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/federated-saml:
|
* /api/v1/federated-saml:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create a SAML Federation app
|
* summary: Create an Identity Federation app
|
||||||
* parameters:
|
* parameters:
|
||||||
* - name: name
|
* - name: name
|
||||||
* description: Name
|
* description: Name
|
||||||
|
@ -113,7 +114,7 @@ export class App {
|
||||||
* required: false
|
* required: false
|
||||||
* type: string
|
* type: string
|
||||||
* - name: tenants
|
* - name: tenants
|
||||||
* description: Mapping of tenants whose connections will be grouped under this SAML Federation app
|
* description: Mapping of tenants whose connections will be grouped under this Identity Federation app
|
||||||
* in: formData
|
* in: formData
|
||||||
* required: false
|
* required: false
|
||||||
* type: array
|
* type: array
|
||||||
|
@ -122,7 +123,17 @@ export class App {
|
||||||
* in: formData
|
* in: formData
|
||||||
* required: false
|
* required: false
|
||||||
* type: array
|
* type: array
|
||||||
* tags: [SAML Federation]
|
* - name: type
|
||||||
|
* description: If creating an OIDC app, this should be set to 'oidc' otherwise it defaults to 'saml'
|
||||||
|
* in: formData
|
||||||
|
* required: false
|
||||||
|
* type: array
|
||||||
|
* - name: redirectUrl
|
||||||
|
* description: If creating an OIDC app, provide the redirect URL
|
||||||
|
* in: formData
|
||||||
|
* required: false
|
||||||
|
* type: array
|
||||||
|
* tags: [Identity Federation]
|
||||||
* produces:
|
* produces:
|
||||||
* - application/json
|
* - application/json
|
||||||
* consumes:
|
* consumes:
|
||||||
|
@ -138,6 +149,8 @@ export class App {
|
||||||
*/
|
*/
|
||||||
public async create({
|
public async create({
|
||||||
name,
|
name,
|
||||||
|
type,
|
||||||
|
redirectUrl,
|
||||||
tenant,
|
tenant,
|
||||||
product,
|
product,
|
||||||
acsUrl,
|
acsUrl,
|
||||||
|
@ -150,16 +163,25 @@ export class App {
|
||||||
}: NewAppParams) {
|
}: NewAppParams) {
|
||||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||||
|
|
||||||
if (!tenant || !product || !acsUrl || !entityId || !name) {
|
if (type === 'oidc') {
|
||||||
throw new JacksonError(
|
if (!tenant || !product || !redirectUrl || !name) {
|
||||||
'Missing required parameters. Required parameters are: name, tenant, product, acsUrl, entityId',
|
throw new JacksonError(
|
||||||
400
|
'Missing required parameters. Required parameters are: name, tenant, product, redirectUrl',
|
||||||
);
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!tenant || !product || !acsUrl || !entityId || !name) {
|
||||||
|
throw new JacksonError(
|
||||||
|
'Missing required parameters. Required parameters are: name, tenant, product, acsUrl, entityId',
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateTenantAndProduct(tenant, product);
|
validateTenantAndProduct(tenant, product);
|
||||||
|
|
||||||
const id = appID(tenant, product);
|
const id = fedAppID(tenant, product, type);
|
||||||
|
|
||||||
// Check if an app already exists for the same tenant and product
|
// Check if an app already exists for the same tenant and product
|
||||||
const foundApp = await this.store.get(id);
|
const foundApp = await this.store.get(id);
|
||||||
|
@ -197,6 +219,8 @@ export class App {
|
||||||
|
|
||||||
const app: SAMLFederationApp = {
|
const app: SAMLFederationApp = {
|
||||||
id,
|
id,
|
||||||
|
type,
|
||||||
|
redirectUrl,
|
||||||
name,
|
name,
|
||||||
tenant,
|
tenant,
|
||||||
product,
|
product,
|
||||||
|
@ -209,18 +233,26 @@ export class App {
|
||||||
mappings: mappings || [],
|
mappings: mappings || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.store.put(
|
if (type === 'oidc') {
|
||||||
id,
|
app.clientID = `${clientIDFederatedPrefix}${id}`;
|
||||||
app,
|
app.clientSecret = crypto.randomBytes(24).toString('hex');
|
||||||
{
|
}
|
||||||
name: IndexNames.EntityID,
|
|
||||||
value: entityId,
|
const indexes = [
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: IndexNames.Product,
|
name: IndexNames.Product,
|
||||||
value: product,
|
value: product,
|
||||||
}
|
},
|
||||||
);
|
];
|
||||||
|
|
||||||
|
if (type !== 'oidc') {
|
||||||
|
indexes.push({
|
||||||
|
name: IndexNames.EntityID,
|
||||||
|
value: entityId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.store.put(id, app, ...indexes);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
@ -229,7 +261,7 @@ export class App {
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/federated-saml:
|
* /api/v1/federated-saml:
|
||||||
* get:
|
* get:
|
||||||
* summary: Get a SAML Federation app
|
* summary: Get an Identity Federation app
|
||||||
* parameters:
|
* parameters:
|
||||||
* - name: id
|
* - name: id
|
||||||
* description: App ID
|
* description: App ID
|
||||||
|
@ -247,7 +279,7 @@ export class App {
|
||||||
* required: false
|
* required: false
|
||||||
* type: string
|
* type: string
|
||||||
* tags:
|
* tags:
|
||||||
* - SAML Federation
|
* - Identity Federation
|
||||||
* produces:
|
* produces:
|
||||||
* - application/json
|
* - application/json
|
||||||
* responses:
|
* responses:
|
||||||
|
@ -263,17 +295,17 @@ export class App {
|
||||||
const app = await this.store.get(params.id);
|
const app = await this.store.get(params.id);
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new JacksonError('SAML Federation app not found', 404);
|
throw new JacksonError('Identity Federation app not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return app as SAMLFederationApp;
|
return app as SAMLFederationApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tenant' in params && 'product' in params) {
|
if ('tenant' in params && 'product' in params) {
|
||||||
const app = await this.store.get(appID(params.tenant, params.product));
|
const app = await this.store.get(fedAppID(params.tenant, params.product, params.type));
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new JacksonError('SAML Federation app not found', 404);
|
throw new JacksonError('Identity Federation app not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return app as SAMLFederationApp;
|
return app as SAMLFederationApp;
|
||||||
|
@ -286,7 +318,7 @@ export class App {
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/federated-saml/product:
|
* /api/v1/federated-saml/product:
|
||||||
* get:
|
* get:
|
||||||
* summary: Get SAML Federation apps by product
|
* summary: Get Identity Federation apps by product
|
||||||
* parameters:
|
* parameters:
|
||||||
* - name: product
|
* - name: product
|
||||||
* description: Product
|
* description: Product
|
||||||
|
@ -294,7 +326,7 @@ export class App {
|
||||||
* required: true
|
* required: true
|
||||||
* type: string
|
* type: string
|
||||||
* tags:
|
* tags:
|
||||||
* - SAML Federation
|
* - Identity Federation
|
||||||
* produces:
|
* produces:
|
||||||
* - application/json
|
* - application/json
|
||||||
* responses:
|
* responses:
|
||||||
|
@ -341,7 +373,7 @@ export class App {
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
if (!apps || apps.length === 0) {
|
if (!apps || apps.length === 0) {
|
||||||
throw new JacksonError('SAML Federation app not found', 404);
|
throw new JacksonError('Identity Federation app not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps[0];
|
return apps[0];
|
||||||
|
@ -351,7 +383,7 @@ export class App {
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/federated-saml:
|
* /api/v1/federated-saml:
|
||||||
* patch:
|
* patch:
|
||||||
* summary: Update a SAML Federation app
|
* summary: Update an Identity Federation app
|
||||||
* parameters:
|
* parameters:
|
||||||
* - name: id
|
* - name: id
|
||||||
* description: App ID
|
* description: App ID
|
||||||
|
@ -394,7 +426,7 @@ export class App {
|
||||||
* required: false
|
* required: false
|
||||||
* type: string
|
* type: string
|
||||||
* - name: tenants
|
* - name: tenants
|
||||||
* description: Mapping of tenants whose connections will be grouped under this SAML Federation app
|
* description: Mapping of tenants whose connections will be grouped under this Identity Federation app
|
||||||
* in: formData
|
* in: formData
|
||||||
* required: false
|
* required: false
|
||||||
* type: array
|
* type: array
|
||||||
|
@ -404,7 +436,7 @@ export class App {
|
||||||
* required: false
|
* required: false
|
||||||
* type: array
|
* type: array
|
||||||
* tags:
|
* tags:
|
||||||
* - SAML Federation
|
* - Identity Federation
|
||||||
* produces:
|
* produces:
|
||||||
* - application/json
|
* - application/json
|
||||||
* consumes:
|
* consumes:
|
||||||
|
@ -419,7 +451,7 @@ export class App {
|
||||||
public async update(params: Partial<SAMLFederationApp>) {
|
public async update(params: Partial<SAMLFederationApp>) {
|
||||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||||
|
|
||||||
const { id, tenant, product } = params;
|
const { id, tenant, product, type } = params;
|
||||||
|
|
||||||
if (!id && (!tenant || !product)) {
|
if (!id && (!tenant || !product)) {
|
||||||
throw new JacksonError('Provide either the `id` or `tenant` and `product` to update the app', 400);
|
throw new JacksonError('Provide either the `id` or `tenant` and `product` to update the app', 400);
|
||||||
|
@ -430,11 +462,11 @@ export class App {
|
||||||
if (id) {
|
if (id) {
|
||||||
app = await this.get({ id });
|
app = await this.get({ id });
|
||||||
} else if (tenant && product) {
|
} else if (tenant && product) {
|
||||||
app = await this.get({ tenant, product });
|
app = await this.get({ tenant, product, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new JacksonError('SAML Federation app not found', 404);
|
throw new JacksonError('Identity Federation app not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toUpdate: Partial<SAMLFederationApp> = {};
|
const toUpdate: Partial<SAMLFederationApp> = {};
|
||||||
|
@ -445,6 +477,10 @@ export class App {
|
||||||
toUpdate['name'] = params.name;
|
toUpdate['name'] = params.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('redirectUrl' in params) {
|
||||||
|
toUpdate['redirectUrl'] = params.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if ('acsUrl' in params) {
|
if ('acsUrl' in params) {
|
||||||
toUpdate['acsUrl'] = params.acsUrl;
|
toUpdate['acsUrl'] = params.acsUrl;
|
||||||
}
|
}
|
||||||
|
@ -516,7 +552,7 @@ export class App {
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/federated-saml:
|
* /api/v1/federated-saml:
|
||||||
* delete:
|
* delete:
|
||||||
* summary: Delete a SAML Federation app
|
* summary: Delete an Identity Federation app
|
||||||
* parameters:
|
* parameters:
|
||||||
* - name: id
|
* - name: id
|
||||||
* description: App ID
|
* description: App ID
|
||||||
|
@ -534,7 +570,7 @@ export class App {
|
||||||
* required: false
|
* required: false
|
||||||
* type: string
|
* type: string
|
||||||
* tags:
|
* tags:
|
||||||
* - SAML Federation
|
* - Identity Federation
|
||||||
* produces:
|
* produces:
|
||||||
* - application/json
|
* - application/json
|
||||||
* responses:
|
* responses:
|
||||||
|
@ -551,7 +587,7 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tenant' in params && 'product' in params) {
|
if ('tenant' in params && 'product' in params) {
|
||||||
const id = appID(params.tenant, params.product);
|
const id = fedAppID(params.tenant, params.product, params.type);
|
||||||
return await this.store.delete(id);
|
return await this.store.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { App } from './app';
|
||||||
import type { JacksonOption, SSOTracerInstance } from '../../typings';
|
import type { JacksonOption, SSOTracerInstance } from '../../typings';
|
||||||
import { SSOHandler } from '../../controller/sso-handler';
|
import { SSOHandler } from '../../controller/sso-handler';
|
||||||
|
|
||||||
// This is the main entry point for the SAML Federation module
|
// This is the main entry point for the Identity Federation module
|
||||||
const SAMLFederation = async ({
|
const SAMLFederation = async ({
|
||||||
db,
|
db,
|
||||||
opts,
|
opts,
|
||||||
|
|
|
@ -9,6 +9,10 @@ export type AttributeMapping = {
|
||||||
|
|
||||||
export type SAMLFederationApp = {
|
export type SAMLFederationApp = {
|
||||||
id: string;
|
id: string;
|
||||||
|
type?: string;
|
||||||
|
clientID?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
redirectUrl?: string[] | string;
|
||||||
name: string;
|
name: string;
|
||||||
tenant: string;
|
tenant: string;
|
||||||
product: string;
|
product: string;
|
||||||
|
@ -37,4 +41,5 @@ export type AppRequestParams =
|
||||||
| {
|
| {
|
||||||
tenant: string;
|
tenant: string;
|
||||||
product: string;
|
product: string;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -110,6 +110,10 @@ export const controllers = async (
|
||||||
// Create default certificate if it doesn't exist.
|
// Create default certificate if it doesn't exist.
|
||||||
await x509.init(certificateStore, opts);
|
await x509.init(certificateStore, opts);
|
||||||
|
|
||||||
|
// Enterprise Features
|
||||||
|
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
||||||
|
const brandingController = new BrandingController({ store: settingsStore, opts });
|
||||||
|
|
||||||
const oauthController = new OAuthController({
|
const oauthController = new OAuthController({
|
||||||
connectionStore,
|
connectionStore,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
|
@ -117,6 +121,7 @@ export const controllers = async (
|
||||||
tokenStore,
|
tokenStore,
|
||||||
ssoTracer,
|
ssoTracer,
|
||||||
opts,
|
opts,
|
||||||
|
samlFedApp: samlFederatedController.app,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logoutController = new LogoutController({
|
const logoutController = new LogoutController({
|
||||||
|
@ -129,10 +134,6 @@ export const controllers = async (
|
||||||
const spConfig = new SPSSOConfig(opts);
|
const spConfig = new SPSSOConfig(opts);
|
||||||
const directorySyncController = await initDirectorySync({ db, opts, eventController });
|
const directorySyncController = await initDirectorySync({ db, opts, eventController });
|
||||||
|
|
||||||
// Enterprise Features
|
|
||||||
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
|
||||||
const brandingController = new BrandingController({ store: settingsStore, opts });
|
|
||||||
|
|
||||||
// write pre-loaded connections if present
|
// write pre-loaded connections if present
|
||||||
const preLoadedConnection = opts.preLoadedConnection || opts.preLoadedConfig;
|
const preLoadedConnection = opts.preLoadedConnection || opts.preLoadedConfig;
|
||||||
if (preLoadedConnection && preLoadedConnection.length > 0) {
|
if (preLoadedConnection && preLoadedConnection.length > 0) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ tap.test('Federated SAML App', async () => {
|
||||||
acsUrl: serviceProvider.acsUrl,
|
acsUrl: serviceProvider.acsUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Should be able to create a new SAML Federation app', async (t) => {
|
tap.test('Should be able to create a new Identity Federation app', async (t) => {
|
||||||
t.ok(app);
|
t.ok(app);
|
||||||
t.match(app.id, appId);
|
t.match(app.id, appId);
|
||||||
t.match(app.tenant, tenant);
|
t.match(app.tenant, tenant);
|
||||||
|
@ -31,14 +31,14 @@ tap.test('Federated SAML App', async () => {
|
||||||
t.match(app.acsUrl, serviceProvider.acsUrl);
|
t.match(app.acsUrl, serviceProvider.acsUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Should be able to get the SAML Federation app by id', async (t) => {
|
tap.test('Should be able to get the Identity Federation app by id', async (t) => {
|
||||||
const response = await samlFederatedController.app.get({ id: app.id });
|
const response = await samlFederatedController.app.get({ id: app.id });
|
||||||
|
|
||||||
t.ok(response);
|
t.ok(response);
|
||||||
t.match(response.id, app.id);
|
t.match(response.id, app.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Should be able to get the SAML Federation app by entity id', async (t) => {
|
tap.test('Should be able to get the Identity Federation app by entity id', async (t) => {
|
||||||
const response = await samlFederatedController.app.getByEntityId(serviceProvider.entityId);
|
const response = await samlFederatedController.app.getByEntityId(serviceProvider.entityId);
|
||||||
|
|
||||||
t.ok(response);
|
t.ok(response);
|
||||||
|
@ -62,7 +62,7 @@ tap.test('Federated SAML App', async () => {
|
||||||
t.match(apps.data[0], app);
|
t.match(apps.data[0], app);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Should be able to update the SAML Federation app', async (t) => {
|
tap.test('Should be able to update the Identity Federation app', async (t) => {
|
||||||
// Update by id
|
// Update by id
|
||||||
const response = await samlFederatedController.app.update({
|
const response = await samlFederatedController.app.update({
|
||||||
id: app.id,
|
id: app.id,
|
||||||
|
@ -135,7 +135,7 @@ tap.test('Federated SAML App', async () => {
|
||||||
t.match(updatedApp.primaryColor, null);
|
t.match(updatedApp.primaryColor, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Should be able to get all SAML Federation apps', async (t) => {
|
tap.test('Should be able to get all Identity Federation apps', async (t) => {
|
||||||
const response = await samlFederatedController.app.getAll({});
|
const response = await samlFederatedController.app.getAll({});
|
||||||
|
|
||||||
t.ok(response);
|
t.ok(response);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
import { DocumentMagnifyingGlassIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
import DocumentMagnifyingGlassIcon from '@heroicons/react/24/outline/DocumentMagnifyingGlassIcon';
|
||||||
|
import WrenchScrewdriverIcon from '@heroicons/react/24/outline/WrenchScrewdriverIcon';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import EmptyState from '@components/EmptyState';
|
import EmptyState from '@components/EmptyState';
|
||||||
import { useProjects } from '@lib/ui/retraced';
|
import { useProjects } from '@lib/ui/retraced';
|
||||||
|
|
|
@ -182,13 +182,14 @@ export const getServerSideProps = async ({ query, locale, req }) => {
|
||||||
|
|
||||||
const paramsToRelay = { ...query } as { [key: string]: string };
|
const paramsToRelay = { ...query } as { [key: string]: string };
|
||||||
|
|
||||||
const { authFlow, entityId, tenant, product, idp_hint, samlFedAppId } = query as {
|
const { authFlow, entityId, tenant, product, idp_hint, samlFedAppId, fedType } = query as {
|
||||||
authFlow: 'sp-initiated' | 'idp-initiated';
|
authFlow: 'sp-initiated' | 'idp-initiated';
|
||||||
tenant?: string;
|
tenant?: string;
|
||||||
product?: string;
|
product?: string;
|
||||||
idp_hint?: string;
|
idp_hint?: string;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
samlFedAppId?: string;
|
samlFedAppId?: string;
|
||||||
|
fedType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!['sp-initiated', 'idp-initiated'].includes(authFlow)) {
|
if (!['sp-initiated', 'idp-initiated'].includes(authFlow)) {
|
||||||
|
@ -200,7 +201,10 @@ export const getServerSideProps = async ({ query, locale, req }) => {
|
||||||
// The user has selected an IdP to continue with
|
// The user has selected an IdP to continue with
|
||||||
if (idp_hint) {
|
if (idp_hint) {
|
||||||
const params = new URLSearchParams(paramsToRelay);
|
const params = new URLSearchParams(paramsToRelay);
|
||||||
const destination = samlFedAppId ? `/api/federated-saml/sso?${params}` : `/api/oauth/authorize?${params}`;
|
const destination =
|
||||||
|
samlFedAppId && fedType !== 'oidc'
|
||||||
|
? `/api/federated-saml/sso?${params}`
|
||||||
|
: `/api/oauth/authorize?${params}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { MDXRemote } from 'next-mdx-remote';
|
||||||
import { serialize } from 'next-mdx-remote/serialize';
|
import { serialize } from 'next-mdx-remote/serialize';
|
||||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||||
import { ArrowsRightLeftIcon } from '@heroicons/react/24/outline';
|
import ArrowsRightLeftIcon from '@heroicons/react/24/outline/ArrowsRightLeftIcon';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
|
|
@ -1142,7 +1142,7 @@
|
||||||
},
|
},
|
||||||
"/api/v1/federated-saml": {
|
"/api/v1/federated-saml": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Create a SAML Federation app",
|
"summary": "Create an Identity Federation app",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
@ -1202,7 +1202,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tenants",
|
"name": "tenants",
|
||||||
"description": "Mapping of tenants whose connections will be grouped under this SAML Federation app",
|
"description": "Mapping of tenants whose connections will be grouped under this Identity Federation app",
|
||||||
"in": "formData",
|
"in": "formData",
|
||||||
"required": false,
|
"required": false,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
@ -1213,10 +1213,24 @@
|
||||||
"in": "formData",
|
"in": "formData",
|
||||||
"required": false,
|
"required": false,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "If creating an OIDC app, this should be set to 'oidc' otherwise it defaults to 'saml'",
|
||||||
|
"in": "formData",
|
||||||
|
"required": false,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "redirectUrl",
|
||||||
|
"description": "If creating an OIDC app, provide the redirect URL",
|
||||||
|
"in": "formData",
|
||||||
|
"required": false,
|
||||||
|
"type": "array"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"SAML Federation"
|
"Identity Federation"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
@ -1238,7 +1252,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Get a SAML Federation app",
|
"summary": "Get an Identity Federation app",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -1263,7 +1277,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"SAML Federation"
|
"Identity Federation"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
@ -1278,7 +1292,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"summary": "Update a SAML Federation app",
|
"summary": "Update an Identity Federation app",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -1338,7 +1352,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tenants",
|
"name": "tenants",
|
||||||
"description": "Mapping of tenants whose connections will be grouped under this SAML Federation app",
|
"description": "Mapping of tenants whose connections will be grouped under this Identity Federation app",
|
||||||
"in": "formData",
|
"in": "formData",
|
||||||
"required": false,
|
"required": false,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
@ -1352,7 +1366,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"SAML Federation"
|
"Identity Federation"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
@ -1371,7 +1385,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"summary": "Delete a SAML Federation app",
|
"summary": "Delete an Identity Federation app",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -1396,7 +1410,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"SAML Federation"
|
"Identity Federation"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
@ -1413,7 +1427,7 @@
|
||||||
},
|
},
|
||||||
"/api/v1/federated-saml/product": {
|
"/api/v1/federated-saml/product": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Get SAML Federation apps by product",
|
"summary": "Get Identity Federation apps by product",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "product",
|
"name": "product",
|
||||||
|
@ -1424,7 +1438,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"SAML Federation"
|
"Identity Federation"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
|
Loading…
Reference in New Issue