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 files = fs.readdirSync('./', { recursive: true, withFileTypes: true });
|
||||
//console.log('files:', files);
|
||||
|
||||
let error = false;
|
||||
|
||||
|
@ -25,7 +24,6 @@ files.forEach((file) => {
|
|||
|
||||
(fileContent.match(regExp) || []).forEach((match) => {
|
||||
const id = match.replace("t('", '').replace("'", '');
|
||||
// console.log('match:', match);
|
||||
allStrings[id] = true;
|
||||
if (!localeFile[id]) {
|
||||
error = true;
|
||||
|
@ -35,7 +33,6 @@ files.forEach((file) => {
|
|||
|
||||
(fileContent.match(altRegExp) || []).forEach((match) => {
|
||||
const id = match.replace("i18nKey='", '').replace("'", '');
|
||||
// console.log('match:', match, id);
|
||||
allStrings[id] = true;
|
||||
if (!localeFile[id]) {
|
||||
error = true;
|
||||
|
|
|
@ -10,7 +10,7 @@ import SSOLogo from '@components/logo/SSO';
|
|||
import DSyncLogo from '@components/logo/DSync';
|
||||
import AuditLogsLogo from '@components/logo/AuditLogs';
|
||||
import Vault from '@components/logo/Vault';
|
||||
import { Cog8ToothIcon } from '@heroicons/react/24/outline';
|
||||
import Cog8ToothIcon from '@heroicons/react/24/outline/Cog8ToothIcon';
|
||||
|
||||
type SidebarProps = {
|
||||
isOpen: boolean;
|
||||
|
|
|
@ -2,7 +2,8 @@ import { ButtonLink } from '@components/ButtonLink';
|
|||
import { Dispatch, FormEvent, SetStateAction, useMemo, useState } from 'react';
|
||||
import { EditViewOnlyFields, getCommonFields } from './fieldCatalog';
|
||||
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 { 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 { 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 { samlFederatedController } = await jackson();
|
||||
|
||||
|
@ -52,7 +52,7 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
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 { 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 { samlFederatedController } = await jackson();
|
||||
|
||||
|
@ -31,7 +31,7 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
return res.status(201).json({ data: app });
|
||||
};
|
||||
|
||||
// Get SAML Federation apps
|
||||
// Get Identity Federation apps
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
|
||||
|
|
|
@ -11,7 +11,11 @@ const AppsList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
<FederatedSAMLApps
|
||||
urls={{ getApps: '/api/admin/federated-saml' }}
|
||||
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 { 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 { fetcher, addQueryParamsToPath } from '../utils';
|
||||
import { DirectoryTab } from '../dsync';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import useSWR from 'swr';
|
||||
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 { addQueryParamsToPath, fetcher } from '../utils';
|
||||
import { DirectoryTab } from '../dsync';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import useSWR from 'swr';
|
||||
import { useState } from 'react';
|
||||
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 { fetcher, addQueryParamsToPath } from '../utils';
|
||||
import { DirectoryTab } from '../dsync';
|
||||
|
|
|
@ -5,8 +5,9 @@ import { useTranslation } from 'next-i18next';
|
|||
import { useFormik } from 'formik';
|
||||
import { Card } from '../shared';
|
||||
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 = ({
|
||||
app,
|
||||
|
@ -23,12 +24,16 @@ export const Edit = ({
|
|||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const connectionIsOIDC = app.type === 'oidc';
|
||||
const connectionIsSAML = !connectionIsOIDC;
|
||||
|
||||
const formik = useFormik<EditApp>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: app.name || '',
|
||||
acsUrl: app.acsUrl || '',
|
||||
tenants: app.tenants || [],
|
||||
redirectUrl: app.redirectUrl || [],
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
const rawResponse = await fetch(urls.patch, {
|
||||
|
@ -92,34 +97,74 @@ export const Edit = ({
|
|||
/>
|
||||
</label>
|
||||
)}
|
||||
<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>
|
||||
<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>
|
||||
{connectionIsOIDC && (
|
||||
<label className='form-control w-full'>
|
||||
<div className='label'>
|
||||
<span className='label-text'>{t('bui-fs-client-id')}</span>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
className='input-bordered input'
|
||||
defaultValue={app.clientID}
|
||||
disabled
|
||||
/>
|
||||
</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='label'>
|
||||
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
||||
|
@ -130,6 +175,7 @@ export const Edit = ({
|
|||
onlyUnique={true}
|
||||
inputProps={{
|
||||
placeholder: t('bui-fs-enter-tenant'),
|
||||
autocomplete: 'off',
|
||||
}}
|
||||
focusedClassName='input-focused'
|
||||
addOnBlur={true}
|
||||
|
|
|
@ -42,6 +42,9 @@ export const EditFederatedSAMLApp = ({
|
|||
|
||||
const app = data?.data;
|
||||
|
||||
const connectionIsOIDC = app.type === 'oidc';
|
||||
const connectionIsSAML = !connectionIsOIDC;
|
||||
|
||||
const deleteApp = async () => {
|
||||
try {
|
||||
await fetch(urls.deleteApp, { method: 'DELETE', headers: defaultHeaders });
|
||||
|
@ -66,15 +69,17 @@ export const EditFederatedSAMLApp = ({
|
|||
}}
|
||||
excludeFields={excludeFields}
|
||||
/>
|
||||
<EditAttributesMapping
|
||||
app={app}
|
||||
urls={{ patch: urls.updateApp }}
|
||||
onError={onError}
|
||||
onUpdate={(data) => {
|
||||
mutate({ data });
|
||||
onUpdate?.(data);
|
||||
}}
|
||||
/>
|
||||
{connectionIsSAML && (
|
||||
<EditAttributesMapping
|
||||
app={app}
|
||||
urls={{ patch: urls.updateApp }}
|
||||
onError={onError}
|
||||
onUpdate={(data) => {
|
||||
mutate({ data });
|
||||
onUpdate?.(data);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<EditBranding
|
||||
app={app}
|
||||
urls={{ patch: urls.updateApp }}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../shared';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
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 { pageLimit } from '../shared/Pagination';
|
||||
import { usePaginate } from '../hooks';
|
||||
|
@ -30,7 +30,7 @@ export const FederatedSAMLApps = ({
|
|||
urls: { getApps: string };
|
||||
excludeFields?: ExcludeFields[];
|
||||
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 }[];
|
||||
}) => {
|
||||
const { router } = useRouter();
|
||||
|
@ -128,8 +128,11 @@ export const FederatedSAMLApps = ({
|
|||
title={t('bui-fs-apps')}
|
||||
actions={
|
||||
<>
|
||||
<LinkOutline href={actions.idpConfiguration} target='_blank' className='btn-md'>
|
||||
{t('bui-fs-idp-config')}
|
||||
<LinkOutline href={actions.oidcConfiguration} target='_blank' className='btn-md'>
|
||||
{t('bui-fs-oidc-config')}
|
||||
</LinkOutline>
|
||||
<LinkOutline href={actions.samlConfiguration} target='_blank' className='btn-md'>
|
||||
{t('bui-fs-saml-config')}
|
||||
</LinkOutline>
|
||||
<ButtonPrimary onClick={() => router?.push(actions.newApp)} className='btn-md'>
|
||||
{t('bui-fs-new-app')}
|
||||
|
|
|
@ -3,14 +3,15 @@ import TagsInput from 'react-tagsinput';
|
|||
import { Card, Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
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 { AttributesMapping } from './AttributesMapping';
|
||||
import { PageHeader } from '../shared';
|
||||
import { ItemList } from '../shared/ItemList';
|
||||
|
||||
type NewSAMLFederationApp = Pick<
|
||||
SAMLFederationApp,
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings'
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||
>;
|
||||
|
||||
export const NewFederatedSAMLApp = ({
|
||||
|
@ -31,6 +32,7 @@ export const NewFederatedSAMLApp = ({
|
|||
const { t } = useTranslation('common');
|
||||
|
||||
const initialValues: NewSAMLFederationApp = {
|
||||
type: 'saml',
|
||||
name: '',
|
||||
tenant: '',
|
||||
product: '',
|
||||
|
@ -65,6 +67,9 @@ export const NewFederatedSAMLApp = ({
|
|||
},
|
||||
});
|
||||
|
||||
const connectionIsOIDC = formik.values.type === 'oidc';
|
||||
const connectionIsSAML = !connectionIsOIDC;
|
||||
|
||||
const generateEntityId = () => {
|
||||
const id = crypto.randomUUID().replace(/-/g, '');
|
||||
const entityId = `${samlAudience}/${id}`;
|
||||
|
@ -78,6 +83,38 @@ export const NewFederatedSAMLApp = ({
|
|||
<PageHeader title={t('bui-fs-create-app')} />
|
||||
<form onSubmit={formik.handleSubmit} method='POST'>
|
||||
<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'>
|
||||
<div className='label'>
|
||||
<span className='label-text'>{t('bui-shared-name')}</span>
|
||||
|
@ -98,7 +135,7 @@ export const NewFederatedSAMLApp = ({
|
|||
</div>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='acme'
|
||||
placeholder='example.com'
|
||||
className='input input-bordered w-full text-sm'
|
||||
name='tenant'
|
||||
value={formik.values.tenant}
|
||||
|
@ -122,49 +159,63 @@ export const NewFederatedSAMLApp = ({
|
|||
/>
|
||||
</label>
|
||||
)}
|
||||
<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>
|
||||
<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>
|
||||
{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>
|
||||
</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='label'>
|
||||
<span className='label-text'>{t('bui-fs-tenants')}</span>
|
||||
|
@ -175,6 +226,7 @@ export const NewFederatedSAMLApp = ({
|
|||
onlyUnique={true}
|
||||
inputProps={{
|
||||
placeholder: t('bui-fs-enter-tenant'),
|
||||
autocomplete: 'off',
|
||||
}}
|
||||
focusedClassName='input-focused'
|
||||
addOnBlur={true}
|
||||
|
@ -183,20 +235,23 @@ export const NewFederatedSAMLApp = ({
|
|||
<span className='label-text-alt'>{t('bui-fs-tenants-mapping-desc')}</span>
|
||||
</label>
|
||||
</label>
|
||||
<label className='form-control w-full'>
|
||||
<div className='label'>
|
||||
<span className='label-text'>{t('bui-fs-attribute-mappings')}</span>
|
||||
</div>
|
||||
<div className='label'>
|
||||
<span className='label-text-alt'>{t('bui-fs-attribute-mappings-desc')}</span>
|
||||
</div>
|
||||
</label>
|
||||
<AttributesMapping
|
||||
mappings={formik.values.mappings || []}
|
||||
onAttributeMappingsChange={(mappings) => formik.setFieldValue('mappings', mappings)}
|
||||
/>
|
||||
{connectionIsSAML && (
|
||||
<label className='form-control w-full'>
|
||||
<div className='label'>
|
||||
<span className='label-text'>{t('bui-fs-attribute-mappings')}</span>
|
||||
</div>
|
||||
<div className='label'>
|
||||
<span className='label-text-alt'>{t('bui-fs-attribute-mappings-desc')}</span>
|
||||
</div>
|
||||
<AttributesMapping
|
||||
mappings={formik.values.mappings || []}
|
||||
onAttributeMappingsChange={(mappings) => formik.setFieldValue('mappings', mappings)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<div className='flex gap-2 justify-end pt-6'>
|
||||
<Button
|
||||
type='submit'
|
||||
className='btn btn-primary btn-md'
|
||||
loading={formik.isSubmitting}
|
||||
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 = {
|
||||
id: string;
|
||||
type?: string;
|
||||
clientID?: string;
|
||||
clientSecret?: string;
|
||||
redirectUrl?: string[] | string;
|
||||
name: string;
|
||||
tenant: string;
|
||||
product: string;
|
||||
|
|
|
@ -48,19 +48,26 @@ export const WellKnownURLs = ({ jacksonUrl }: { jacksonUrl?: string }) => {
|
|||
type: 'auth',
|
||||
},
|
||||
{
|
||||
title: t('bui-wku-idp-metadata'),
|
||||
description: t('bui-wku-idp-metadata-desc'),
|
||||
title: t('bui-wku-saml-idp-metadata'),
|
||||
description: t('bui-wku-saml-idp-metadata-desc'),
|
||||
href: `${baseUrl}/.well-known/idp-metadata`,
|
||||
buttonText: viewText,
|
||||
type: 'saml-fed',
|
||||
},
|
||||
{
|
||||
title: t('bui-wku-idp-configuration'),
|
||||
description: t('bui-wku-idp-config-desc'),
|
||||
title: t('bui-wku-saml-idp-configuration'),
|
||||
description: t('bui-wku-saml-idp-config-desc'),
|
||||
href: `${baseUrl}/.well-known/idp-configuration`,
|
||||
buttonText: viewText,
|
||||
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 (
|
||||
|
|
|
@ -55,10 +55,10 @@
|
|||
"webhook_secret": "Webhook secret",
|
||||
"webhook_url": "Webhook URL",
|
||||
"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",
|
||||
"saml_federation_update_success": "SAML Federation app updated successfully.",
|
||||
"saml_federation_delete_success": "SAML federation app deleted successfully",
|
||||
"saml_federation_update_success": "Identity Federation app updated successfully.",
|
||||
"saml_federation_delete_success": "Identity federation app deleted successfully",
|
||||
"saml_federation_app_info": "SAML Federation App Information",
|
||||
"saml_federation_app_info_details": "Choose from the following options to configure your SAML Federation on the service provider side",
|
||||
"download_metadata": "Download Metadata",
|
||||
|
@ -68,7 +68,7 @@
|
|||
"directory_created_successfully": "Directory created successfully",
|
||||
"directory_updated_successfully": "Directory updated successfully",
|
||||
"dashboard": "Dashboard",
|
||||
"saml_federation": "SAML Federation",
|
||||
"saml_federation": "Identity Federation",
|
||||
"sso_tracer": "SSO Tracer",
|
||||
"settings": "Settings",
|
||||
"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-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-idp-metadata": "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-idp-configuration": "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-metadata": "SAML IdP Metadata",
|
||||
"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-saml-idp-configuration": "SAML Federation IdP Configuration",
|
||||
"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-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-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.",
|
||||
|
@ -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-attribute-mappings": "Attribute Mappings",
|
||||
"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-edit-app": "Edit SAML Federation App",
|
||||
"bui-fs-edit-app": "Edit Identity Federation App",
|
||||
"bui-fs-sp-attribute": "SP Attribute",
|
||||
"bui-fs-idp-attribute": "IdP Attribute",
|
||||
"bui-fs-add-mapping": "Add Mapping",
|
||||
"bui-fs-add-another": "Add another",
|
||||
"bui-fs-no-apps": "No SAML Federation Apps found.",
|
||||
"bui-fs-no-apps-desc": "Create a new SAML Federation App to configure SAML Federation.",
|
||||
"bui-fs-add": "Add",
|
||||
"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-entity-id": "Entity ID / Audience URI / Audience Restriction",
|
||||
"bui-fs-branding-title": "Customize Look and Feel",
|
||||
|
@ -228,11 +236,12 @@
|
|||
"bui-fs-primary-color": "Primary Color",
|
||||
"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-delete-app-title": "Delete this SAML Federation app",
|
||||
"bui-fs-delete-app-desc": "This action cannot be undone. This will permanently delete the SAML Federation app.",
|
||||
"bui-fs-apps": "SAML Federation Apps",
|
||||
"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 Identity Federation app.",
|
||||
"bui-fs-apps": "Identity Federation Apps",
|
||||
"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-oidc-attributes": "OIDC Attributes",
|
||||
"bui-dsync-name": "Name",
|
||||
|
@ -313,6 +322,7 @@
|
|||
"bui-sl-expiry-days": "Expiry in days",
|
||||
"bui-sl-default-redirect-url": "Default redirect URL",
|
||||
"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-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",
|
||||
|
|
|
@ -3176,8 +3176,7 @@
|
|||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
|
||||
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
@ -4809,6 +4808,18 @@
|
|||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
|
@ -4930,18 +4941,6 @@
|
|||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz",
|
||||
|
@ -6327,8 +6326,7 @@
|
|||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.4.0.tgz",
|
||||
"integrity": "sha512-MdFHsyb1a/Ee0H3NmzWTSLqchacDV/APF0H6BNQvraWrOiIocys2EmTFZPgHxWhcfO94c1F34I9MACU7x0hHKA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0",
|
||||
"bson": "^6.4.0",
|
||||
|
@ -7734,11 +7732,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
|
||||
"integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.6",
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
|
@ -8882,9 +8880,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
|
||||
"integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
|
||||
"version": "0.3.24",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz",
|
||||
"integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { deflateRaw } from 'zlib';
|
|||
import saml from '@boxyhq/saml20';
|
||||
import { TokenSet, errors, generators } from 'openid-client';
|
||||
import { SAMLProfile } from '@boxyhq/saml20/dist/typings';
|
||||
import { clientIDFederatedPrefix, clientIDOIDCPrefix } from './utils';
|
||||
|
||||
import type {
|
||||
IOAuthController,
|
||||
|
@ -20,6 +21,7 @@ import type {
|
|||
SSOTracerInstance,
|
||||
OAuthErrorHandlerParams,
|
||||
OIDCAuthzResponsePayload,
|
||||
SAMLFederationApp,
|
||||
} from '../typings';
|
||||
import {
|
||||
relayStatePrefix,
|
||||
|
@ -43,6 +45,7 @@ import { getDefaultCertificate } from '../saml/x509';
|
|||
import { SSOHandler } from './sso-handler';
|
||||
import { ValidateOption, extractSAMLResponseAttributes } from '../saml/lib';
|
||||
import { oidcIssuerInstance } from './oauth/oidc-issuer';
|
||||
import { App } from '../ee/federated-saml/app';
|
||||
|
||||
const deflateRawAsync = promisify(deflateRaw);
|
||||
|
||||
|
@ -54,14 +57,16 @@ export class OAuthController implements IOAuthController {
|
|||
private ssoTracer: SSOTracerInstance;
|
||||
private opts: JacksonOption;
|
||||
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.sessionStore = sessionStore;
|
||||
this.codeStore = codeStore;
|
||||
this.tokenStore = tokenStore;
|
||||
this.ssoTracer = ssoTracer;
|
||||
this.opts = opts;
|
||||
this.samlFedApp = samlFedApp;
|
||||
|
||||
this.ssoHandler = new SSOHandler({
|
||||
connection: connectionStore,
|
||||
|
@ -90,6 +95,7 @@ export class OAuthController implements IOAuthController {
|
|||
let requestedScopes: string[] | undefined;
|
||||
let requestedOIDCFlow: boolean | undefined;
|
||||
let connection: SAMLSSORecord | OIDCSSORecord | undefined;
|
||||
let fedApp: SAMLFederationApp | undefined;
|
||||
|
||||
try {
|
||||
const tenant = 'tenant' in body ? body.tenant : undefined;
|
||||
|
@ -167,10 +173,40 @@ export class OAuthController implements IOAuthController {
|
|||
connection = response.connection;
|
||||
}
|
||||
} else {
|
||||
connection = await this.connectionStore.get(client_id);
|
||||
if (connection) {
|
||||
requestedTenant = connection.tenant;
|
||||
requestedProduct = connection.product;
|
||||
// client_id is not encoded, so we look for the connection using the client_id
|
||||
// First we check if it's a federated connection
|
||||
if (client_id.startsWith(`${clientIDFederatedPrefix}${clientIDOIDCPrefix}`)) {
|
||||
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 {
|
||||
|
@ -182,7 +218,13 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
|
||||
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) {
|
||||
const error_description = getErrorMessage(err);
|
||||
|
@ -399,6 +441,10 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
if (idp_hint) {
|
||||
requested.idp_hint = idp_hint;
|
||||
} else {
|
||||
if (fedApp) {
|
||||
requested.idp_hint = connection.clientID;
|
||||
}
|
||||
}
|
||||
if (requestedOIDCFlow) {
|
||||
requested.oidc = true;
|
||||
|
@ -417,6 +463,7 @@ export class OAuthController implements IOAuthController {
|
|||
code_challenge,
|
||||
code_challenge_method,
|
||||
requested,
|
||||
oidcFederated: fedApp ? { redirectUrl: fedApp.redirectUrl, id: fedApp.id } : undefined,
|
||||
};
|
||||
await this.sessionStore.put(
|
||||
sessionId,
|
||||
|
@ -495,6 +542,7 @@ export class OAuthController implements IOAuthController {
|
|||
let issuer: string | undefined;
|
||||
let isIdPFlow: boolean | undefined;
|
||||
let isSAMLFederated: boolean | undefined;
|
||||
let isOIDCFederated: boolean | undefined;
|
||||
let validateOpts: ValidateOption;
|
||||
let redirect_uri: string | undefined;
|
||||
const { SAMLResponse, idp_hint, RelayState = '' } = body;
|
||||
|
@ -536,6 +584,7 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
|
||||
isSAMLFederated = session && 'samlFederated' in session;
|
||||
isOIDCFederated = session && 'oidcFederated' in session;
|
||||
const isSPFlow = !isIdPFlow && !isSAMLFederated;
|
||||
|
||||
// IdP initiated SSO flow
|
||||
|
@ -564,10 +613,11 @@ export class OAuthController implements IOAuthController {
|
|||
|
||||
// SP initiated SSO flow
|
||||
// Resolve if there are multiple matches for SP login
|
||||
if (isSPFlow || isSAMLFederated) {
|
||||
if (isSPFlow || isSAMLFederated || isOIDCFederated) {
|
||||
connection = connections.filter((c) => {
|
||||
return (
|
||||
c.clientID === session.requested.client_id ||
|
||||
c.clientID === session.requested.idp_hint ||
|
||||
(c.tenant === session.requested.tenant && c.product === session.requested.product)
|
||||
);
|
||||
})[0];
|
||||
|
@ -582,7 +632,13 @@ export class OAuthController implements IOAuthController {
|
|||
session.redirect_uri &&
|
||||
!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();
|
||||
|
@ -696,6 +752,7 @@ export class OAuthController implements IOAuthController {
|
|||
let oidcConnection: OIDCSSORecord | undefined;
|
||||
let session: any;
|
||||
let isSAMLFederated: boolean | undefined;
|
||||
let isOIDCFederated: boolean | undefined;
|
||||
let redirect_uri: string | undefined;
|
||||
let profile;
|
||||
|
||||
|
@ -714,6 +771,7 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
|
||||
isSAMLFederated = session && 'samlFederated' in session;
|
||||
isOIDCFederated = session && 'oidcFederated' in session;
|
||||
|
||||
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[])) {
|
||||
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) {
|
||||
|
@ -744,6 +808,7 @@ export class OAuthController implements IOAuthController {
|
|||
redirectUri: redirect_uri,
|
||||
relayState: RelayState,
|
||||
isSAMLFederated: !!isSAMLFederated,
|
||||
isOIDCFederated: !!isOIDCFederated,
|
||||
requestedOIDCFlow: !!session?.requested?.oidc,
|
||||
},
|
||||
});
|
||||
|
@ -803,6 +868,7 @@ export class OAuthController implements IOAuthController {
|
|||
redirectUri: redirect_uri,
|
||||
relayState: RelayState,
|
||||
isSAMLFederated: !!isSAMLFederated,
|
||||
isOIDCFederated: !!isOIDCFederated,
|
||||
acsUrl: session.requested.acsUrl,
|
||||
entityId: session.requested.entityId,
|
||||
requestedOIDCFlow: !!session.requested.oidc,
|
||||
|
|
|
@ -11,9 +11,22 @@ export const redirect = (redirectUrl: string, redirectUrls: string[]): boolean =
|
|||
for (const idx in redirectUrls) {
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ export class SSOHandler {
|
|||
entityId?: string;
|
||||
idp_hint?: string;
|
||||
samlFedAppId?: string;
|
||||
fedType?: string;
|
||||
tenants?: string[]; // Only used for SAML IdP initiated flow
|
||||
}): Promise<
|
||||
| {
|
||||
|
@ -67,9 +68,22 @@ export class SSOHandler {
|
|||
entityId,
|
||||
tenants,
|
||||
samlFedAppId = '',
|
||||
fedType = '',
|
||||
} = params;
|
||||
|
||||
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
|
||||
if (tenants && tenants.length > 0 && product) {
|
||||
|
@ -99,36 +113,27 @@ export class SSOHandler {
|
|||
connections = result.data;
|
||||
}
|
||||
|
||||
const noSSOConnectionErrMessage = 'No SSO connection found.';
|
||||
|
||||
if (!connections || connections.length === 0) {
|
||||
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 (connections.length > 1) {
|
||||
const url = new URL(`${this.opts.externalUrl}${this.opts.idpDiscoveryPath}`);
|
||||
|
||||
// SP initiated flow
|
||||
if (['oauth', 'saml'].includes(authFlow) && tenant && product) {
|
||||
const params = new URLSearchParams({
|
||||
tenant,
|
||||
product,
|
||||
if (['oauth', 'saml'].includes(authFlow)) {
|
||||
const qps = {
|
||||
authFlow: 'sp-initiated',
|
||||
samlFedAppId,
|
||||
fedType,
|
||||
...originalParams,
|
||||
});
|
||||
};
|
||||
if (tenant && product && fedType !== 'oidc') {
|
||||
qps['tenant'] = tenant;
|
||||
qps['product'] = product;
|
||||
}
|
||||
const params = new URLSearchParams(qps);
|
||||
|
||||
return { redirectUrl: `${url}?${params}` };
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ export const storeNamespacePrefix = {
|
|||
};
|
||||
|
||||
export const relayStatePrefix = 'boxyhq_jackson_';
|
||||
export const clientIDFederatedPrefix = 'fed_';
|
||||
export const clientIDOIDCPrefix = 'oidc_';
|
||||
|
||||
export const validateAbsoluteUrl = (url, message) => {
|
||||
try {
|
||||
|
@ -302,6 +304,10 @@ export const appID = (tenant: string, product: string) => {
|
|||
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
|
||||
const wellKnownProviders = {
|
||||
'okta.com': 'Okta',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import crypto from 'crypto';
|
||||
import type {
|
||||
Storable,
|
||||
JacksonOption,
|
||||
|
@ -6,7 +7,7 @@ import type {
|
|||
GetByProductParams,
|
||||
AppRequestParams,
|
||||
} from '../../typings';
|
||||
import { appID } from '../../controller/utils';
|
||||
import { fedAppID, clientIDFederatedPrefix } from '../../controller/utils';
|
||||
import { createMetadataXML } from '../../saml/lib';
|
||||
import { JacksonError } from '../../controller/error';
|
||||
import { getDefaultCertificate } from '../../saml/x509';
|
||||
|
@ -15,7 +16,7 @@ import { throwIfInvalidLicense } from '../common/checkLicense';
|
|||
|
||||
type NewAppParams = Pick<
|
||||
SAMLFederationApp,
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings'
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||
> & {
|
||||
logoUrl?: string;
|
||||
faviconUrl?: string;
|
||||
|
@ -70,7 +71,7 @@ export class App {
|
|||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* post:
|
||||
* summary: Create a SAML Federation app
|
||||
* summary: Create an Identity Federation app
|
||||
* parameters:
|
||||
* - name: name
|
||||
* description: Name
|
||||
|
@ -113,7 +114,7 @@ export class App {
|
|||
* required: false
|
||||
* type: string
|
||||
* - 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
|
||||
* required: false
|
||||
* type: array
|
||||
|
@ -122,7 +123,17 @@ export class App {
|
|||
* in: formData
|
||||
* required: false
|
||||
* 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:
|
||||
* - application/json
|
||||
* consumes:
|
||||
|
@ -138,6 +149,8 @@ export class App {
|
|||
*/
|
||||
public async create({
|
||||
name,
|
||||
type,
|
||||
redirectUrl,
|
||||
tenant,
|
||||
product,
|
||||
acsUrl,
|
||||
|
@ -150,16 +163,25 @@ export class App {
|
|||
}: NewAppParams) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
if (!tenant || !product || !acsUrl || !entityId || !name) {
|
||||
throw new JacksonError(
|
||||
'Missing required parameters. Required parameters are: name, tenant, product, acsUrl, entityId',
|
||||
400
|
||||
);
|
||||
if (type === 'oidc') {
|
||||
if (!tenant || !product || !redirectUrl || !name) {
|
||||
throw new JacksonError(
|
||||
'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);
|
||||
|
||||
const id = appID(tenant, product);
|
||||
const id = fedAppID(tenant, product, type);
|
||||
|
||||
// Check if an app already exists for the same tenant and product
|
||||
const foundApp = await this.store.get(id);
|
||||
|
@ -197,6 +219,8 @@ export class App {
|
|||
|
||||
const app: SAMLFederationApp = {
|
||||
id,
|
||||
type,
|
||||
redirectUrl,
|
||||
name,
|
||||
tenant,
|
||||
product,
|
||||
|
@ -209,18 +233,26 @@ export class App {
|
|||
mappings: mappings || [],
|
||||
};
|
||||
|
||||
await this.store.put(
|
||||
id,
|
||||
app,
|
||||
{
|
||||
name: IndexNames.EntityID,
|
||||
value: entityId,
|
||||
},
|
||||
if (type === 'oidc') {
|
||||
app.clientID = `${clientIDFederatedPrefix}${id}`;
|
||||
app.clientSecret = crypto.randomBytes(24).toString('hex');
|
||||
}
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
name: IndexNames.Product,
|
||||
value: product,
|
||||
}
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
if (type !== 'oidc') {
|
||||
indexes.push({
|
||||
name: IndexNames.EntityID,
|
||||
value: entityId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.store.put(id, app, ...indexes);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -229,7 +261,7 @@ export class App {
|
|||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* get:
|
||||
* summary: Get a SAML Federation app
|
||||
* summary: Get an Identity Federation app
|
||||
* parameters:
|
||||
* - name: id
|
||||
* description: App ID
|
||||
|
@ -247,7 +279,7 @@ export class App {
|
|||
* required: false
|
||||
* type: string
|
||||
* tags:
|
||||
* - SAML Federation
|
||||
* - Identity Federation
|
||||
* produces:
|
||||
* - application/json
|
||||
* responses:
|
||||
|
@ -263,17 +295,17 @@ export class App {
|
|||
const app = await this.store.get(params.id);
|
||||
|
||||
if (!app) {
|
||||
throw new JacksonError('SAML Federation app not found', 404);
|
||||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
return app as SAMLFederationApp;
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new JacksonError('SAML Federation app not found', 404);
|
||||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
return app as SAMLFederationApp;
|
||||
|
@ -286,7 +318,7 @@ export class App {
|
|||
* @swagger
|
||||
* /api/v1/federated-saml/product:
|
||||
* get:
|
||||
* summary: Get SAML Federation apps by product
|
||||
* summary: Get Identity Federation apps by product
|
||||
* parameters:
|
||||
* - name: product
|
||||
* description: Product
|
||||
|
@ -294,7 +326,7 @@ export class App {
|
|||
* required: true
|
||||
* type: string
|
||||
* tags:
|
||||
* - SAML Federation
|
||||
* - Identity Federation
|
||||
* produces:
|
||||
* - application/json
|
||||
* responses:
|
||||
|
@ -341,7 +373,7 @@ export class App {
|
|||
).data;
|
||||
|
||||
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];
|
||||
|
@ -351,7 +383,7 @@ export class App {
|
|||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* patch:
|
||||
* summary: Update a SAML Federation app
|
||||
* summary: Update an Identity Federation app
|
||||
* parameters:
|
||||
* - name: id
|
||||
* description: App ID
|
||||
|
@ -394,7 +426,7 @@ export class App {
|
|||
* required: false
|
||||
* type: string
|
||||
* - 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
|
||||
* required: false
|
||||
* type: array
|
||||
|
@ -404,7 +436,7 @@ export class App {
|
|||
* required: false
|
||||
* type: array
|
||||
* tags:
|
||||
* - SAML Federation
|
||||
* - Identity Federation
|
||||
* produces:
|
||||
* - application/json
|
||||
* consumes:
|
||||
|
@ -419,7 +451,7 @@ export class App {
|
|||
public async update(params: Partial<SAMLFederationApp>) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const { id, tenant, product } = params;
|
||||
const { id, tenant, product, type } = params;
|
||||
|
||||
if (!id && (!tenant || !product)) {
|
||||
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) {
|
||||
app = await this.get({ id });
|
||||
} else if (tenant && product) {
|
||||
app = await this.get({ tenant, product });
|
||||
app = await this.get({ tenant, product, type });
|
||||
}
|
||||
|
||||
if (!app) {
|
||||
throw new JacksonError('SAML Federation app not found', 404);
|
||||
throw new JacksonError('Identity Federation app not found', 404);
|
||||
}
|
||||
|
||||
const toUpdate: Partial<SAMLFederationApp> = {};
|
||||
|
@ -445,6 +477,10 @@ export class App {
|
|||
toUpdate['name'] = params.name;
|
||||
}
|
||||
|
||||
if ('redirectUrl' in params) {
|
||||
toUpdate['redirectUrl'] = params.redirectUrl;
|
||||
}
|
||||
|
||||
if ('acsUrl' in params) {
|
||||
toUpdate['acsUrl'] = params.acsUrl;
|
||||
}
|
||||
|
@ -516,7 +552,7 @@ export class App {
|
|||
* @swagger
|
||||
* /api/v1/federated-saml:
|
||||
* delete:
|
||||
* summary: Delete a SAML Federation app
|
||||
* summary: Delete an Identity Federation app
|
||||
* parameters:
|
||||
* - name: id
|
||||
* description: App ID
|
||||
|
@ -534,7 +570,7 @@ export class App {
|
|||
* required: false
|
||||
* type: string
|
||||
* tags:
|
||||
* - SAML Federation
|
||||
* - Identity Federation
|
||||
* produces:
|
||||
* - application/json
|
||||
* responses:
|
||||
|
@ -551,7 +587,7 @@ export class App {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { App } from './app';
|
|||
import type { JacksonOption, SSOTracerInstance } from '../../typings';
|
||||
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 ({
|
||||
db,
|
||||
opts,
|
||||
|
|
|
@ -9,6 +9,10 @@ export type AttributeMapping = {
|
|||
|
||||
export type SAMLFederationApp = {
|
||||
id: string;
|
||||
type?: string;
|
||||
clientID?: string;
|
||||
clientSecret?: string;
|
||||
redirectUrl?: string[] | string;
|
||||
name: string;
|
||||
tenant: string;
|
||||
product: string;
|
||||
|
@ -37,4 +41,5 @@ export type AppRequestParams =
|
|||
| {
|
||||
tenant: string;
|
||||
product: string;
|
||||
type?: string;
|
||||
};
|
||||
|
|
|
@ -110,6 +110,10 @@ export const controllers = async (
|
|||
// Create default certificate if it doesn't exist.
|
||||
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({
|
||||
connectionStore,
|
||||
sessionStore,
|
||||
|
@ -117,6 +121,7 @@ export const controllers = async (
|
|||
tokenStore,
|
||||
ssoTracer,
|
||||
opts,
|
||||
samlFedApp: samlFederatedController.app,
|
||||
});
|
||||
|
||||
const logoutController = new LogoutController({
|
||||
|
@ -129,10 +134,6 @@ export const controllers = async (
|
|||
const spConfig = new SPSSOConfig(opts);
|
||||
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
|
||||
const preLoadedConnection = opts.preLoadedConnection || opts.preLoadedConfig;
|
||||
if (preLoadedConnection && preLoadedConnection.length > 0) {
|
||||
|
|
|
@ -22,7 +22,7 @@ tap.test('Federated SAML App', async () => {
|
|||
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.match(app.id, appId);
|
||||
t.match(app.tenant, tenant);
|
||||
|
@ -31,14 +31,14 @@ tap.test('Federated SAML App', async () => {
|
|||
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 });
|
||||
|
||||
t.ok(response);
|
||||
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);
|
||||
|
||||
t.ok(response);
|
||||
|
@ -62,7 +62,7 @@ tap.test('Federated SAML App', async () => {
|
|||
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
|
||||
const response = await samlFederatedController.app.update({
|
||||
id: app.id,
|
||||
|
@ -135,7 +135,7 @@ tap.test('Federated SAML App', async () => {
|
|||
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({});
|
||||
|
||||
t.ok(response);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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 EmptyState from '@components/EmptyState';
|
||||
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 { 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';
|
||||
tenant?: string;
|
||||
product?: string;
|
||||
idp_hint?: string;
|
||||
entityId?: string;
|
||||
samlFedAppId?: string;
|
||||
fedType?: string;
|
||||
};
|
||||
|
||||
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
|
||||
if (idp_hint) {
|
||||
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 {
|
||||
redirect: {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { MDXRemote } from 'next-mdx-remote';
|
|||
import { serialize } from 'next-mdx-remote/serialize';
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||
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 { useTranslation } from 'next-i18next';
|
||||
|
||||
|
|
|
@ -1142,7 +1142,7 @@
|
|||
},
|
||||
"/api/v1/federated-saml": {
|
||||
"post": {
|
||||
"summary": "Create a SAML Federation app",
|
||||
"summary": "Create an Identity Federation app",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
|
@ -1202,7 +1202,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
"required": false,
|
||||
"type": "array"
|
||||
|
@ -1213,10 +1213,24 @@
|
|||
"in": "formData",
|
||||
"required": false,
|
||||
"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": [
|
||||
"SAML Federation"
|
||||
"Identity Federation"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1238,7 +1252,7 @@
|
|||
}
|
||||
},
|
||||
"get": {
|
||||
"summary": "Get a SAML Federation app",
|
||||
"summary": "Get an Identity Federation app",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -1263,7 +1277,7 @@
|
|||
}
|
||||
],
|
||||
"tags": [
|
||||
"SAML Federation"
|
||||
"Identity Federation"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1278,7 +1292,7 @@
|
|||
}
|
||||
},
|
||||
"patch": {
|
||||
"summary": "Update a SAML Federation app",
|
||||
"summary": "Update an Identity Federation app",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -1338,7 +1352,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
"required": false,
|
||||
"type": "array"
|
||||
|
@ -1352,7 +1366,7 @@
|
|||
}
|
||||
],
|
||||
"tags": [
|
||||
"SAML Federation"
|
||||
"Identity Federation"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1371,7 +1385,7 @@
|
|||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete a SAML Federation app",
|
||||
"summary": "Delete an Identity Federation app",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -1396,7 +1410,7 @@
|
|||
}
|
||||
],
|
||||
"tags": [
|
||||
"SAML Federation"
|
||||
"Identity Federation"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
@ -1413,7 +1427,7 @@
|
|||
},
|
||||
"/api/v1/federated-saml/product": {
|
||||
"get": {
|
||||
"summary": "Get SAML Federation apps by product",
|
||||
"summary": "Get Identity Federation apps by product",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "product",
|
||||
|
@ -1424,7 +1438,7 @@
|
|||
}
|
||||
],
|
||||
"tags": [
|
||||
"SAML Federation"
|
||||
"Identity Federation"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
|
Loading…
Reference in New Issue