0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console,phrases,core): add SSO connector creation guide (#4866)

This commit is contained in:
Darcy Ye 2023-11-19 14:30:22 +08:00 committed by GitHub
parent 5832b30276
commit 57655dfeb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2203 additions and 81 deletions

View file

@ -119,6 +119,8 @@ function ConsoleContent() {
<Route path="enterprise-sso">
<Route index element={<EnterpriseSsoConnectors />} />
<Route path="create" element={<EnterpriseSsoConnectors />} />
<Route path=":id/guide" element={<EnterpriseSsoConnectors />} />
<Route path=":id" element={<EnterpriseSsoConnectors />} />
</Route>
)}
<Route path="webhooks">

View file

@ -0,0 +1,5 @@
@use '@/scss/underscore' as _;
.copyToClipboard {
display: block;
}

View file

@ -0,0 +1,79 @@
import { SsoProviderName, type SsoConnectorWithProviderConfig } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext } from 'react';
import { z } from 'zod';
import { AppDataContext } from '@/contexts/AppDataProvider';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import FormField from '@/ds-components/FormField';
import useCustomDomain from '@/hooks/use-custom-domain';
import * as styles from './index.module.scss';
type Props = {
ssoConnectorId: string;
} & Pick<SsoConnectorWithProviderConfig, 'providerName' | 'providerProperties'>;
/**
* TODO: Should align this with the guard `samlServiceProviderMetadataGuard` defined in {@link logto/core/src/sso/types/saml.ts}.
* This only applies to SAML SSO connectors.
*/
const providerPropertiesGuard = z.object({
assertionConsumerServiceUrl: z.string().min(1),
entityId: z.string().min(1),
});
function BasicInfo({ ssoConnectorId, providerName, providerProperties }: Props) {
const { tenantEndpoint } = useContext(AppDataContext);
const { applyDomain: applyCustomDomain } = useCustomDomain();
if (providerName === SsoProviderName.OIDC) {
return (
<div>
<FormField title="enterprise_sso.basic_info.oidc.redirect_uri_field_name">
{/* Generated and passed in by Admin console. */}
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
value={applyCustomDomain(
new URL(`/callback/${ssoConnectorId}`, tenantEndpoint).toString()
)}
/>
</FormField>
</div>
);
}
const result = providerPropertiesGuard.safeParse(providerProperties);
/**
* Used fallback to show the default value anyways but the cases should not happen.
* TODO: @darcyYe refactor to remove the manual guard.
*/
return (
<div>
<FormField title="enterprise_sso.basic_info.saml.acs_url_field_name">
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
value={applyCustomDomain(
conditional(result.success && result.data.assertionConsumerServiceUrl) ??
new URL(`/api/authn/saml/sso/${ssoConnectorId}`, tenantEndpoint).toString()
)}
/>
</FormField>
<FormField title="enterprise_sso.basic_info.saml.audience_uri_field_name">
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
value={applyCustomDomain(
conditional(result.success && result.data.entityId) ??
new URL(`/api/sso-connector/${ssoConnectorId}`, tenantEndpoint).toString()
)}
/>
</FormField>
</div>
);
}
export default BasicInfo;

View file

@ -0,0 +1,29 @@
import { useFormContext } from 'react-hook-form';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import { type OidcGuideFormType } from '../../types.js';
function OidcMetadataForm() {
const { register } = useFormContext<OidcGuideFormType>();
return (
<div>
<FormField title="enterprise_sso.metadata.oidc.client_id_field_name">
<TextInput {...register('clientId')} />
</FormField>
<FormField title="enterprise_sso.metadata.oidc.client_secret_field_name">
<TextInput {...register('clientSecret')} />
</FormField>
<FormField title="enterprise_sso.metadata.oidc.issuer_field_name">
<TextInput {...register('issuer')} />
</FormField>
<FormField title="enterprise_sso.metadata.oidc.scope_field_name">
<TextInput {...register('scope')} />
</FormField>
</div>
);
}
export default OidcMetadataForm;

View file

@ -0,0 +1,39 @@
@use '@/scss/underscore' as _;
.copyToClipboard {
display: block;
}
.table {
width: 100%;
}
.row {
width: 100%;
display: flex;
flex-direction: row;
gap: _.unit(4);
padding-bottom: _.unit(3);
> td,
th {
width: calc((100% - _.unit(4)) / 2);
}
}
.header {
width: 100%;
font: var(--font-title-3);
> tr {
padding-bottom: _.unit(3);
}
* > th {
text-align: left;
}
}
.body {
width: 100%;
}

View file

@ -0,0 +1,74 @@
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import DynamicT from '@/ds-components/DynamicT';
import TextInput from '@/ds-components/TextInput';
import { attributeKeys, type SamlGuideFormType, type AttributeMapping } from '../../types.js';
import * as styles from './index.module.scss';
type Props = {
isReadOnly?: boolean;
};
const primaryKey = 'attributeMapping';
function SamlAttributeMapping({ isReadOnly }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { watch, register } = useFormContext<SamlGuideFormType>();
// eslint-disable-next-line react-hooks/exhaustive-deps
const attributeMapping = watch(primaryKey) ?? {};
const attributeMappingEntries = useMemo<
Array<[keyof AttributeMapping, string | undefined]>
>(() => {
return attributeKeys.map((key) => [key, attributeMapping[key] ?? '']);
}, [attributeMapping]);
return (
<div>
<table className={styles.table}>
<thead className={styles.header}>
<tr className={styles.row}>
<th>
<DynamicT forKey="enterprise_sso.attribute_mapping.col_sp_claims" />
</th>
<th>
<DynamicT forKey="enterprise_sso.attribute_mapping.col_idp_claims" />
</th>
</tr>
</thead>
<tbody className={styles.body}>
{attributeMappingEntries.map(([key, value]) => {
return (
<tr key={key} className={styles.row}>
<td>
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
value={key}
/>
</td>
<td>
{isReadOnly ? (
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
value={value ?? ''}
/>
) : (
<TextInput {...register(`${primaryKey}.${key}`)} placeholder={key} />
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
export default SamlAttributeMapping;

View file

@ -0,0 +1,35 @@
@use '@/scss/underscore' as _;
.dropdownIcon {
color: var(--color-text-link);
}
.dropdown {
min-width: unset;
}
.icon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: transparent;
&.selected {
color: var(--color-primary-40);
}
}
.title {
display: flex;
align-items: center;
margin-left: _.unit(1);
.optionText {
font: var(--font-body-2);
margin-left: _.unit(3);
}
&.selected {
color: var(--color-primary-40);
}
}

View file

@ -0,0 +1,72 @@
import { type AdminConsoleKey } from '@logto/phrases';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import SwitchArrowIcon from '@/assets/icons/switch-arrow.svg';
import Tick from '@/assets/icons/tick.svg';
import ActionMenu from '@/ds-components/ActionMenu';
import { DropdownItem } from '@/ds-components/Dropdown';
import DynamicT from '@/ds-components/DynamicT';
import * as styles from './index.module.scss';
type Props = {
value: FormFormat;
onChange: (formFormat: FormFormat) => void;
};
export enum FormFormat {
Url = 'url',
Xml = 'xml',
Manual = 'manual',
}
type Options = {
value: FormFormat;
title: AdminConsoleKey;
};
const options: Options[] = [
{ value: FormFormat.Url, title: 'enterprise_sso.metadata.metadata_format_url' },
{ value: FormFormat.Xml, title: 'enterprise_sso.metadata.metadata_format_xml' },
{ value: FormFormat.Manual, title: 'enterprise_sso.metadata.metadata_format_manual' },
];
function SwitchFormatButton({ value, onChange }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<ActionMenu
buttonProps={{
type: 'text',
size: 'small',
title: 'enterprise_sso.metadata.dropdown_trigger_text',
icon: <SwitchArrowIcon className={styles.dropdownIcon} />,
}}
dropdownHorizontalAlign="start"
dropdownClassName={styles.dropdown}
title={t('enterprise_sso.metadata.dropdown_title')}
>
{options.map(({ value: optionValue, title }) => (
<DropdownItem
key={optionValue}
onClick={() => {
onChange(optionValue);
}}
>
<Tick className={classNames(styles.icon, value === optionValue && styles.selected)} />
<div
className={classNames(
styles.title,
styles.optionText,
value === optionValue && styles.selected
)}
>
<DynamicT forKey={title} />
</div>
</DropdownItem>
))}
</ActionMenu>
);
}
export default SwitchFormatButton;

View file

@ -0,0 +1,13 @@
@use '@/scss/underscore' as _;
.description {
color: var(--color-text-secondary);
font: var(--font-body-2);
margin-top: _.unit(0.5);
}
.samlMetadataForm {
> div:not(:first-child) {
margin-top: _.unit(6);
}
}

View file

@ -0,0 +1,108 @@
import { conditional, pick } from '@silverhand/essentials';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import Textarea from '@/ds-components/Textarea';
import { type SamlGuideFormType } from '../../types.js';
import SwitchFormatButton, { FormFormat } from './SwitchFormatButton';
import * as styles from './index.module.scss';
/**
* Since we need to reset some of the form fields when we switch the form format
* for SAML configuration form, here we define this object for convenience.
*/
const completeResetObject: Omit<SamlGuideFormType, 'attributeMapping'> = {
metadata: undefined,
metadataUrl: undefined,
signInEndpoint: undefined,
entityId: undefined,
x509Certificate: undefined,
};
type Props = {
formFormat: FormFormat;
};
function SamlMetadataFormFields({ formFormat }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { register } = useFormContext<SamlGuideFormType>();
switch (formFormat) {
case FormFormat.Manual: {
return (
<>
<FormField isRequired title="enterprise_sso.metadata.saml.sign_in_endpoint_field_name">
<TextInput {...register('signInEndpoint')} />
</FormField>
<FormField isRequired title="enterprise_sso.metadata.saml.idp_entity_id_field_name">
<TextInput {...register('entityId')} />
</FormField>
<FormField isRequired title="enterprise_sso.metadata.saml.certificate_field_name">
<Textarea
{...register('x509Certificate')}
placeholder={t('enterprise_sso.metadata.saml.certificate_placeholder')}
/>
</FormField>
</>
);
}
case FormFormat.Xml: {
return (
<FormField title="enterprise_sso.metadata.saml.metadata_xml_field_name">
<Textarea rows={5} {...register('metadata')} />
</FormField>
);
}
case FormFormat.Url: {
return (
<FormField isRequired title="enterprise_sso.metadata.saml.metadata_url_field_name">
<TextInput {...register('metadataUrl')} />
<div className={styles.description}>
{t('enterprise_sso.metadata.saml.metadata_url_description')}
</div>
</FormField>
);
}
default: {
return null;
}
}
}
function SamlMetadataForm() {
const { reset } = useFormContext<SamlGuideFormType>();
const [formFormat, setFormFormat] = useState<FormFormat>(FormFormat.Url);
// If some form format is selected, then fields from other fields should be cleared.
const onSelect = (formFormat: FormFormat) => {
reset({
...conditional(
formFormat === FormFormat.Url &&
pick(completeResetObject, 'metadata', 'entityId', 'signInEndpoint', 'x509Certificate')
),
...conditional(
formFormat === FormFormat.Xml &&
pick(completeResetObject, 'metadataUrl', 'entityId', 'signInEndpoint', 'x509Certificate')
),
...conditional(
formFormat === FormFormat.Manual && pick(completeResetObject, 'metadata', 'metadataUrl')
),
});
setFormFormat(formFormat);
};
return (
<div className={styles.samlMetadataForm}>
<SamlMetadataFormFields formFormat={formFormat} />
<SwitchFormatButton value={formFormat} onChange={onSelect} />
</div>
);
}
export default SamlMetadataForm;

View file

@ -0,0 +1,129 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
background-color: var(--color-base);
height: 100vh;
overflow-x: auto;
.header {
display: flex;
align-items: center;
background: none;
height: 64px;
padding: 0 _.unit(21) 0 _.unit(2);
button {
margin-left: _.unit(4);
}
.separator {
@include _.vertical-bar;
height: 20px;
margin: 0 _.unit(5) 0 _.unit(4);
}
.closeIcon {
color: var(--color-text-secondary);
}
}
.content {
flex: 1;
display: flex;
overflow: auto;
justify-content: center;
min-width: min-content;
padding: _.unit(2) _.unit(6) _.unit(6);
> * {
flex: 1;
max-width: 800px;
min-width: 400px;
}
.readme {
display: flex;
flex-direction: column;
background-color: var(--color-layer-1);
border: 1.5px solid var(--color-focused-variant);
border-radius: 16px;
margin: 0 _.unit(6) 0 0;
overflow-y: auto;
position: sticky;
top: 0;
.readmeTitle {
font: var(--font-title-2);
padding: _.unit(5) _.unit(6) _.unit(4);
border-bottom: 1px solid var(--color-focused-variant);
}
.readmeContent {
flex: 1;
padding: 0 _.unit(6) _.unit(4);
h2 {
color: var(--color-text);
}
h3 {
color: var(--color-text-secondary);
}
}
}
.setup {
padding-bottom: _.unit(6);
.block {
background-color: var(--color-layer-1);
border-radius: 16px;
padding: 0 _.unit(6) _.unit(6);
margin-bottom: _.unit(4);
.blockTitle {
font: var(--font-title-2);
padding: _.unit(5) 0 _.unit(6);
display: flex;
flex-direction: column;
gap: _.unit(2);
.numberedTitle {
display: flex;
align-items: center;
flex-direction: row;
gap: _.unit(4);
}
.number {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--color-focused-variant);
color: var(--color-primary);
font: var(--font-title-2);
text-align: center;
line-height: 28px;
}
.blockSubtitle {
font: var(--font-body-2);
color: var(--color-text-secondary);
}
}
}
.footer {
padding-bottom: _.unit(10);
display: flex;
justify-content: right;
}
}
form + div {
margin-top: _.unit(6);
}
}
}

View file

@ -0,0 +1,187 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { SsoProviderName, type SsoConnectorWithProviderConfig } from '@logto/schemas';
import cleanDeep from 'clean-deep';
import type { ReactNode } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import Modal from 'react-modal';
import Close from '@/assets/icons/close.svg';
import Markdown from '@/components/Markdown';
import Button from '@/ds-components/Button';
import CardTitle from '@/ds-components/CardTitle';
import DangerousRaw from '@/ds-components/DangerousRaw';
import DynamicT from '@/ds-components/DynamicT';
import IconButton from '@/ds-components/IconButton';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
import { splitMarkdownByTitle } from '../../Connectors/utils.js';
import { type GuideFormType } from '../types.js';
import BasicInfo from './BasicInfo';
import OidcMetadataForm from './OidcMetadataForm';
import SamlAttributeMapping from './SamlAttributeMapping';
import SamlMetadataForm from './SamlMetadataForm';
import * as styles from './index.module.scss';
import { type SsoConnectorWithProviderConfigWithGeneric } from './types.js';
type Props<T extends SsoProviderName> = {
isOpen: boolean;
connector: SsoConnectorWithProviderConfigWithGeneric<T>;
onClose: (ssoConnectorId?: string) => void;
isReadOnly?: boolean;
};
type GuideCardProps = {
cardOrder: number;
children: ReactNode;
title: AdminConsoleKey;
description: AdminConsoleKey;
};
function GuideCard({ cardOrder, title, description, children }: GuideCardProps) {
return (
<div className={styles.block}>
<div className={styles.blockTitle}>
<div className={styles.numberedTitle}>
<div className={styles.number}>{cardOrder}</div>
<DynamicT forKey={title} />
</div>
<div className={styles.blockSubtitle}>
<DynamicT forKey={description} />
</div>
</div>
{children}
</div>
);
}
function Guide<T extends SsoProviderName>({ isOpen, connector, onClose, isReadOnly }: Props<T>) {
const {
id: ssoConnectorId,
connectorName: ssoConnectorName,
providerName,
providerProperties,
} = connector;
const api = useApi();
const methods = useForm<GuideFormType<T>>();
const {
formState: { isSubmitting },
handleSubmit,
} = methods;
// TODO: @darcyYe Add SSO connector README.
const { title, content } = splitMarkdownByTitle(
'# SSO connector guide\n\nThis is a guide for Logto Enterprise SSO connector.'
);
const onSubmit = handleSubmit(
trySubmitSafe(async (formData) => {
if (isSubmitting) {
return;
}
await api
.patch(`api/sso-connectors/${ssoConnectorId}/config`, {
json: cleanDeep(formData),
})
.json<SsoConnectorWithProviderConfig>();
onClose(ssoConnectorId);
})
);
return (
<Modal
shouldCloseOnEsc
isOpen={isOpen}
className={modalStyles.fullScreen}
onRequestClose={() => {
onClose();
}}
>
<div className={styles.container}>
<div className={styles.header}>
<IconButton
size="large"
onClick={() => {
onClose(ssoConnectorId);
}}
>
<Close className={styles.closeIcon} />
</IconButton>
<div className={styles.separator} />
<CardTitle
size="small"
title={<DangerousRaw>{ssoConnectorName}</DangerousRaw>}
subtitle="enterprise_sso.guide.subtitle"
/>
</div>
<div className={styles.content}>
<OverlayScrollbar className={styles.readme}>
<div className={styles.readmeTitle}>README: {title}</div>
<Markdown className={styles.readmeContent}>{content}</Markdown>
</OverlayScrollbar>
<div className={styles.setup}>
<FormProvider {...methods}>
<form autoComplete="off" onSubmit={onSubmit}>
<GuideCard
cardOrder={1}
title="enterprise_sso.basic_info.title"
description="enterprise_sso.basic_info.description"
>
<BasicInfo
ssoConnectorId={ssoConnectorId}
providerName={providerName}
providerProperties={providerProperties}
/>
</GuideCard>
{providerName === SsoProviderName.OIDC ? (
<GuideCard
cardOrder={2}
title="enterprise_sso.metadata.title"
description="enterprise_sso.metadata.description"
>
<OidcMetadataForm />
</GuideCard>
) : (
<>
<GuideCard
cardOrder={2}
title="enterprise_sso.attribute_mapping.title"
description="enterprise_sso.attribute_mapping.description"
>
<SamlAttributeMapping isReadOnly={isReadOnly} />
</GuideCard>
<GuideCard
cardOrder={3}
title="enterprise_sso.metadata.title"
description="enterprise_sso.metadata.description"
>
<SamlMetadataForm />
</GuideCard>
</>
)}
<div className={styles.footer}>
<Button
title="enterprise_sso.guide.finish_button_text"
type="primary"
htmlType="submit"
isLoading={isSubmitting}
/>
</div>
</form>
</FormProvider>
</div>
</div>
</div>
</Modal>
);
}
export default Guide;

View file

@ -0,0 +1,9 @@
import { type SsoProviderName, type SsoConnectorWithProviderConfig } from '@logto/schemas';
// Help the Guide component type to be inferred from the connector's type.
export type SsoConnectorWithProviderConfigWithGeneric<T extends SsoProviderName> = Omit<
SsoConnectorWithProviderConfig,
'providerName'
> & {
providerName: T;
};

View file

@ -1,4 +1,7 @@
import { type SsoConnectorFactoriesResponse, type SsoConnector } from '@logto/schemas';
import {
type SsoConnectorFactoriesResponse,
type SsoConnectorWithProviderConfig,
} from '@logto/schemas';
import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@ -22,7 +25,7 @@ import * as styles from './index.module.scss';
type Props = {
isOpen: boolean;
onClose: (ssoConnectorId?: string) => void;
onClose: (ssoConnector?: SsoConnectorWithProviderConfig) => void;
};
type FormType = {
@ -61,10 +64,10 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
);
// `rawOnClose` does not clean the state of the modal.
const onClose = (ssoConnectorId?: string) => {
const onClose = (ssoConnector?: SsoConnectorWithProviderConfig) => {
setSelectedProviderName(undefined);
reset();
rawOnClose(ssoConnectorId);
rawOnClose(ssoConnector);
};
const handleSsoSelection = (providerName: string) => {
@ -79,9 +82,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
const createdSsoConnector = await api
.post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } })
.json<SsoConnector>();
.json<SsoConnectorWithProviderConfig>();
onClose(createdSsoConnector.id);
onClose(createdSsoConnector);
})
);

View file

@ -1,8 +1,9 @@
import { withAppInsights } from '@logto/app-insights/react';
import { type SsoConnectorWithProviderConfig, Theme } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';
import useSWR from 'swr';
import Plus from '@/assets/icons/plus.svg';
@ -21,21 +22,24 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { buildUrl } from '@/utils/url';
import Guide from './Guide';
import SsoCreationModal from './SsoCreationModal';
import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const enterpriseSsoPathname = '/enterprise-sso';
const createEnterpriseSsoPathname = `${enterpriseSsoPathname}/create`;
const buildGuidePathname = (id: string) => `${enterpriseSsoPathname}/${id}/guide`;
const buildDetailsPathname = (id: string) => `${enterpriseSsoPathname}/${id}`;
const buildGuidePathname = (id: string) => `${buildDetailsPathname(id)}/guide`;
function EnterpriseSsoConnectors() {
const theme = useTheme();
const { pathname } = useLocation();
const { navigate } = useTenantPathname();
const { id } = useParams();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [connectorForGuide, setConnectorForGuide] = useState<SsoConnectorWithProviderConfig>();
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
page: 1,
});
@ -52,6 +56,15 @@ function EnterpriseSsoConnectors() {
const isLoading = !data && !error;
const [ssoConnectors, totalCount] = data ?? [];
useEffect(() => {
const selectedSsoConnector = ssoConnectors?.find(
({ id: ssoConnectorId }) => ssoConnectorId === id
);
if (selectedSsoConnector) {
setConnectorForGuide(selectedSsoConnector);
}
}, [id, ssoConnectors]);
return (
<ListPage
title={{
@ -63,7 +76,7 @@ function EnterpriseSsoConnectors() {
ssoConnectors?.length && {
title: 'enterprise_sso.create',
onClick: () => {
navigate({ pathname: createEnterpriseSsoPathname });
navigate(createEnterpriseSsoPathname);
},
}
)}
@ -159,7 +172,7 @@ function EnterpriseSsoConnectors() {
size="large"
icon={<Plus />}
onClick={() => {
navigate({ pathname: createEnterpriseSsoPathname });
navigate(createEnterpriseSsoPathname);
}}
/>
}
@ -168,17 +181,38 @@ function EnterpriseSsoConnectors() {
onRetry: async () => mutate(undefined, true),
}}
widgets={
<SsoCreationModal
isOpen={pathname.endsWith(createEnterpriseSsoPathname)}
onClose={async (id) => {
if (id) {
navigate(buildGuidePathname(id), { replace: true });
return;
}
<>
<SsoCreationModal
isOpen={pathname.endsWith(createEnterpriseSsoPathname)}
onClose={async (ssoConnector) => {
if (ssoConnector) {
await mutate([[...(ssoConnectors ?? []), ssoConnector], totalCount ?? 0 + 1]);
navigate(buildGuidePathname(ssoConnector.id));
return;
}
navigate(enterpriseSsoPathname);
}}
/>
navigate(enterpriseSsoPathname);
}}
/>
{
/** Add this filter to make TypeScript happy, if `connectorForGuide` does not exist, the route will not come to this path. */
connectorForGuide && (
<Guide
isOpen={Boolean(pathname.endsWith(buildGuidePathname(connectorForGuide.id)))}
connector={connectorForGuide}
isReadOnly={connectorForGuide.providerName !== 'SAML'}
onClose={async (connectorId) => {
if (connectorId) {
navigate(buildDetailsPathname(connectorId), { replace: true });
return;
}
navigate(enterpriseSsoPathname);
}}
/>
)
}
</>
}
/>
);

View file

@ -0,0 +1,44 @@
/**
* Type definitions for Enterprise SSO guide form, since the type of SAML config is defined in
* @logto/core and can not be imported here, should align with SAML config types.
* See {@link @logto/core/packages/core/src/sso/SamlConnector/index.ts}.
*/
import { type SsoProviderName } from '@logto/schemas';
export type AttributeMapping = {
id?: string;
email?: string;
phone?: string;
name?: string;
avatar?: string;
};
export const attributeKeys = Object.freeze([
'id',
'email',
'phone',
'name',
'avatar',
]) satisfies ReadonlyArray<keyof AttributeMapping>;
export type SamlGuideFormType = {
metadataUrl?: string;
metadata?: string;
signInEndpoint?: string;
entityId?: string;
x509Certificate?: string;
attributeMapping?: AttributeMapping;
};
export type OidcGuideFormType = {
clientId?: string;
clientSecret?: string;
issuer?: string;
scope?: string;
};
export type GuideFormType<T extends SsoProviderName> = T extends SsoProviderName.OIDC
? OidcGuideFormType
: T extends SsoProviderName.SAML
? SamlGuideFormType
: never;

View file

@ -1,6 +1,4 @@
import { type SsoConnector } from '@logto/schemas';
import { SsoProviderName } from '#src/sso/types/index.js';
import { type SsoConnector, SsoProviderName } from '@logto/schemas';
export const mockSsoConnector = {
id: 'mock-sso-connector',

View file

@ -1,8 +1,8 @@
import { type SupportedSsoConnector } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import RequestError from '#src/errors/RequestError/index.js';
import { ssoConnectorFactories } from '#src/sso/index.js';
import { type SupportedSsoConnector } from '#src/sso/types/index.js';
import { isSupportedSsoConnector } from '#src/sso/utils.js';
import type Queries from '#src/tenants/Queries.js';

View file

@ -1,6 +1,11 @@
import { ConnectorError, type SocialUserInfo } from '@logto/connector-kit';
import { validateRedirectUrl } from '@logto/core-kit';
import { InteractionEvent, type User, type UserSsoIdentity } from '@logto/schemas';
import {
InteractionEvent,
type User,
type UserSsoIdentity,
type SupportedSsoConnector,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
import { z } from 'zod';
@ -9,10 +14,7 @@ import RequestError from '#src/errors/RequestError/index.js';
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
import { type WithInteractionDetailsContext } from '#src/routes/interaction/middleware/koa-interaction-details.js';
import { ssoConnectorFactories } from '#src/sso/index.js';
import {
type SupportedSsoConnector,
type SingleSignOnConnectorSession,
} from '#src/sso/types/index.js';
import { type SingleSignOnConnectorSession } from '#src/sso/types/index.js';
import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';

View file

@ -5,7 +5,7 @@ import {
ssoConnectorWithProviderConfigGuard,
} from '@logto/schemas';
import { generateStandardShortId } from '@logto/shared';
import { conditional, assert } from '@silverhand/essentials';
import { conditional, assert, yes } from '@silverhand/essentials';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
@ -235,21 +235,33 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
`${pathname}/:id/config`,
koaGuard({
params: z.object({ id: z.string().min(1) }),
/**
* Allow partially validate the connector config, on the guide page after
* the SSO connector is created, we allow users to save incomplete config without validating it.
*/
query: z.object({ partialValidateConfig: z.string().optional() }),
body: jsonObjectGuard,
response: ssoConnectorWithProviderConfigGuard,
status: [200, 404, 422],
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { body } = ctx.guard;
const {
params: { id },
body,
query: { partialValidateConfig },
} = ctx.guard;
const { providerName, config } = await getSsoConnectorById(id);
// Merge with existing config and revalidate
const parsedConfig = parseConnectorConfig(providerName, {
...config,
...body,
});
const parsedConfig = parseConnectorConfig(
providerName,
{
...config,
...body,
},
yes(partialValidateConfig)
);
const connector = await ssoConnectors.updateById(id, {
config: parsedConfig,

View file

@ -1,8 +1,8 @@
import { SsoProviderName } from '@logto/schemas';
import { createMockUtils } from '@logto/shared/esm';
import { mockSsoConnector } from '#src/__mocks__/sso.js';
import RequestError from '#src/errors/RequestError/index.js';
import { SsoProviderName } from '#src/sso/types/index.js';
const { jest } = import.meta;
const { mockEsmWithActual } = createMockUtils(jest);

View file

@ -1,5 +1,10 @@
import { type I18nPhrases } from '@logto/connector-kit';
import { type JsonObject, type SsoConnectorWithProviderConfig } from '@logto/schemas';
import type {
JsonObject,
SsoConnectorWithProviderConfig,
SupportedSsoConnector,
SsoProviderName,
} from '@logto/schemas';
import { conditional, trySafe } from '@silverhand/essentials';
import RequestError from '#src/errors/RequestError/index.js';
@ -8,7 +13,6 @@ import {
ssoConnectorFactories,
singleSignOnDomainBlackList,
} from '#src/sso/index.js';
import { type SupportedSsoConnector, type SsoProviderName } from '#src/sso/types/index.js';
const isKeyOfI18nPhrases = (key: string, phrases: I18nPhrases): key is keyof I18nPhrases =>
key in phrases;
@ -71,10 +75,16 @@ export const fetchConnectorProviderDetails = async (
return instance.getConfig();
});
const providerProperties = trySafe(() => {
const instance = new constructor(connector, tenantId);
return instance.getProperties();
});
return {
...connector,
providerLogo: logo,
...conditional(providerConfig && { providerConfig }),
...conditional(providerProperties && { providerProperties }),
};
};

View file

@ -1,7 +1,7 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { SsoProviderName } from '@logto/schemas';
import { mockSsoConnector } from '#src/__mocks__/sso.js';
import { SsoProviderName } from '#src/sso/types/index.js';
import { oidcSsoConnectorFactory } from './index.js';

View file

@ -1,7 +1,5 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { type SsoConnector } from '@logto/schemas';
import { SsoProviderName } from '#src/sso/types/index.js';
import { type SsoConnector, SsoProviderName } from '@logto/schemas';
import OidcConnector from '../OidcConnector/index.js';
import { type SingleSignOnFactory } from '../index.js';
@ -23,6 +21,10 @@ export class OidcSsoConnector extends OidcConnector implements SingleSignOn {
return this._data;
}
// OIDC connector doesn't have additional properties.
// eslint-disable-next-line unicorn/no-useless-undefined
getProperties = () => undefined;
getConfig = async () => this.getOidcConfig();
getIssuer = async () => this.issuer;
}

View file

@ -35,6 +35,7 @@ import {
* @property assertionConsumerServiceUrl The SAML connector's assertion consumer service URL {@link file://src/routes/authn.ts}
* @property _samlIdpMetadataXml The cached raw SAML metadata (in XML-format) from the raw SAML SSO connector config
*
* @method getSamlSpProperties Get the SAML service provider properties.
* @method getSamlIdpMetadata Parse and return SAML config from the SAML connector config. Throws error if config is invalid.
* @method parseSamlAssertion Parse and store the SAML assertion from IdP.
* @method getSingleSignOnUrl Get the SAML SSO URL.
@ -69,6 +70,13 @@ class SamlConnector {
};
}
/**
* @returns Properties of the SAML service provider.
*/
getSamlSpProperties() {
return this.serviceProviderMetadata;
}
/**
* Return parsed SAML metadata.
*

View file

@ -1,7 +1,7 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { SsoProviderName } from '@logto/schemas';
import { mockSsoConnector as _mockSsoConnector } from '#src/__mocks__/sso.js';
import { SsoProviderName } from '#src/sso/types/index.js';
import { samlSsoConnectorFactory } from './index.js';

View file

@ -1,13 +1,13 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { type SsoConnector } from '@logto/schemas';
import { SsoProviderName } from '@logto/schemas';
import { SsoProviderName } from '#src/sso/types/index.js';
import assertThat from '#src/utils/assert-that.js';
import SamlConnector from '../SamlConnector/index.js';
import { type SingleSignOnFactory } from '../index.js';
import { type SingleSignOn } from '../types/index.js';
import { samlConnectorConfigGuard } from '../types/saml.js';
import { type SamlServiceProviderMetadata, samlConnectorConfigGuard } from '../types/saml.js';
import {
type SingleSignOnConnectorSession,
type CreateSingleSignOnSession,
@ -20,6 +20,7 @@ import {
*
* @property data The SAML connector data from the database
*
* @method getProperties Get the SAML service provider properties.
* @method getConfig Get parsed SAML config along with it's metadata. Throws error if config is invalid.
* @method getUserInfo Get social user info.
*/
@ -44,6 +45,13 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
return entityId;
}
/**
* @returns Properties of the SAML service provider.
*/
getProperties(): SamlServiceProviderMetadata {
return this.getSamlSpProperties();
}
/**
* Get parsed SAML connector's config along with it's metadata. Throws error if config is invalid.
*

View file

@ -1,6 +1,5 @@
import { type I18nPhrases } from '@logto/connector-kit';
import { SsoProviderName } from '#src/sso/types/index.js';
import { SsoProviderName } from '@logto/schemas';
import { oidcSsoConnectorFactory, type OidcSsoConnector } from './OidcSsoConnector/index.js';
import { type SamlSsoConnector, samlSsoConnectorFactory } from './SamlSsoConnector/index.js';

View file

@ -1,4 +1,5 @@
import { type JsonObject, type SsoConnector } from '@logto/schemas';
import { type Optional } from '@silverhand/essentials';
export * from './session.js';
@ -13,13 +14,5 @@ export abstract class SingleSignOn {
abstract data: SsoConnector;
abstract getConfig: () => Promise<JsonObject>;
abstract getIssuer: () => Promise<string>;
abstract getProperties: () => Optional<JsonObject>;
}
export enum SsoProviderName {
OIDC = 'OIDC',
SAML = 'SAML',
}
export type SupportedSsoConnector = Omit<SsoConnector, 'providerName'> & {
providerName: SsoProviderName;
};

View file

@ -1,4 +1,4 @@
import { SsoProviderName } from '#src/sso/types/index.js';
import { SsoProviderName } from '@logto/schemas';
import { isSupportedSsoProvider } from './utils.js';

View file

@ -1,6 +1,4 @@
import type { SsoConnector } from '@logto/schemas';
import type { SupportedSsoConnector, SsoProviderName } from '#src/sso/types/index.js';
import type { SsoConnector, SupportedSsoConnector, SsoProviderName } from '@logto/schemas';
import { ssoConnectorFactories } from './index.js';

View file

@ -1,4 +1,5 @@
import { type CreateSsoConnector, type SsoConnector } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { authedAdminApi } from '#src/api/api.js';
@ -44,9 +45,14 @@ export const patchSsoConnectorById = async (id: string, data: Partial<SsoConnect
})
.json<SsoConnectorWithProviderConfig>();
export const patchSsoConnectorConfigById = async (id: string, data: Record<string, unknown>) =>
export const patchSsoConnectorConfigById = async (
id: string,
data: Record<string, unknown>,
partialValidate = false
) =>
authedAdminApi
.patch(`sso-connectors/${id}/config`, {
json: data,
...conditional(partialValidate && { searchParams: { partialValidateConfig: 'true' } }),
})
.json<SsoConnectorWithProviderConfig>();

View file

@ -1,4 +1,9 @@
import { type CreateSsoConnector, SignInIdentifier, demoAppApplicationId } from '@logto/schemas';
import {
type CreateSsoConnector,
SignInIdentifier,
demoAppApplicationId,
SsoProviderName,
} from '@logto/schemas';
import { appendPath, getEnv } from '@silverhand/essentials';
export const logtoUrl = getEnv('INTEGRATION_TESTS_LOGTO_URL', 'http://localhost:3001');
@ -26,11 +31,6 @@ export const consoleUsername = 'svhd';
export const consolePassword = 'silverhandasd_1';
export const mockSocialAuthPageUrl = 'http://mock.social.com';
export enum SsoProviderName {
OIDC = 'OIDC',
SAML = 'SAML',
}
export const newOidcSsoConnectorPayload = {
providerName: SsoProviderName.OIDC,
connectorName: 'test-oidc',

View file

@ -1,9 +1,9 @@
import { InteractionEvent, type SsoConnectorMetadata } from '@logto/schemas';
import { InteractionEvent, SsoProviderName, type SsoConnectorMetadata } from '@logto/schemas';
import { getSsoAuthorizationUrl, getSsoConnectorsByEmail } from '#src/api/interaction-sso.js';
import { putInteraction } from '#src/api/interaction.js';
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
import { logtoUrl, SsoProviderName } from '#src/constants.js';
import { logtoUrl } from '#src/constants.js';
import { initClient } from '#src/helpers/client.js';
describe('Single Sign On Happy Path', () => {

View file

@ -1,4 +1,4 @@
import { InteractionEvent } from '@logto/schemas';
import { InteractionEvent, SsoProviderName } from '@logto/schemas';
import {
partialConfigAndProviderNames,
@ -7,7 +7,6 @@ import {
import { getSsoAuthorizationUrl, postSamlAssertion } from '#src/api/interaction-sso.js';
import { putInteraction } from '#src/api/interaction.js';
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
import { SsoProviderName } from '#src/constants.js';
import { initClient } from '#src/helpers/client.js';
import { expectRejects } from '#src/helpers/index.js';

View file

@ -1,3 +1,5 @@
import { SsoProviderName } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { HTTPError } from 'got';
import {
@ -13,7 +15,6 @@ import {
patchSsoConnectorById,
patchSsoConnectorConfigById,
} from '#src/api/sso-connector.js';
import { SsoProviderName } from '#src/constants.js';
describe('sso-connector library', () => {
it('should return sso-connector-factories', async () => {
@ -300,6 +301,33 @@ describe('patch sso-connector config by id', () => {
await deleteSsoConnectorById(id);
});
it.each(providerNames)(
'should successfully patch incomplete config if `partialValidateConfig` query parameter is specified',
async (providerName) => {
const { id } = await createSsoConnector({
providerName,
connectorName: 'integration_test connector',
config: {
clientSecret: 'bar',
metadataType: 'URL',
},
});
await expect(
patchSsoConnectorConfigById(
id,
{
...conditional(providerName === SsoProviderName.OIDC && { issuer: 'issuer' }),
...conditional(providerName === SsoProviderName.SAML && { entityId: 'entity_id' }),
},
true
)
).resolves.not.toThrow();
await deleteSsoConnectorById(id);
}
);
it.each(partialConfigAndProviderNames)(
'should patch sso connector config',
async ({ providerName, config }) => {

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Name für den Unternehmensidentitätsanbieter',
create_button_text: 'Connector erstellen',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -19,6 +19,56 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Name for the enterprise identity provider',
create_button_text: 'Create connector',
},
guide: {
subtitle: 'A step by step guide to connect the enterprise identity provider.',
finish_button_text: 'Continue',
},
basic_info: {
title: 'Configure your service in the IdP',
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
title: 'Attribute mappings',
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
col_sp_claims: 'Claim name of Logto',
col_idp_claims: 'Claim name of identity provider',
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
title: 'Configure the IdP metadata',
description: 'Configure the metadata from the identity provider',
dropdown_trigger_text: 'Use another configuration method',
dropdown_title: 'select your configuration method',
metadata_format_url: 'Enter the metadata URL',
metadata_format_xml: 'Upload the metadata XML file',
metadata_format_manual: 'Enter metadata details manually',
saml: {
metadata_url_field_name: 'Metadata URL',
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
metadata_xml_field_name: 'Metadata XML file',
metadata_xml_uploader_text: 'Upload metadata XML file',
sign_in_endpoint_field_name: 'Sign on URL',
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
certificate_field_name: 'Signing certificate',
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
client_id_field_name: 'Client ID',
client_secret_field_name: 'Client secret',
issuer_field_name: 'Issuer',
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Nombre para el proveedor de identidad empresarial',
create_button_text: 'Crear conector',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: "Nom du fournisseur d'identité de l'entreprise",
create_button_text: 'Créer un connecteur',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Nome del provider di identità aziendale',
create_button_text: 'Crea connettore',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: '企業向けアイデンティティプロバイダーの名前',
create_button_text: 'コネクターを作成',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -19,6 +19,87 @@ const enterprise_sso = {
connector_name_field_placeholder: '기업 신원 공급자의 이름',
create_button_text: '커넥터 생성',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Nazwa dostawcy tożsamości przedsiębiorstwa',
create_button_text: 'Stwórz łącznik',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Nome do provedor de identidade empresarial',
create_button_text: 'Criar conector',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Nome do provedor de identidade empresarial',
create_button_text: 'Criar conector',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Имя для поставщика идентификации предприятия',
create_button_text: 'Создать коннектор',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -20,6 +20,87 @@ const enterprise_sso = {
connector_name_field_placeholder: 'Kurumsal kimlik sağlayıcı için isim',
create_button_text: 'Bağlayıcı Oluştur',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -19,6 +19,87 @@ const enterprise_sso = {
connector_name_field_placeholder: '企业身份提供者的名称',
create_button_text: '创建连接器',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -19,6 +19,87 @@ const enterprise_sso = {
connector_name_field_placeholder: '企業身份提供者的名稱',
create_button_text: '創建連接器',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -19,6 +19,87 @@ const enterprise_sso = {
connector_name_field_placeholder: '企業身份提供者的名稱',
create_button_text: '創建連接器',
},
guide: {
/** UNTRANSLATED */
subtitle: 'A step by step guide to connect the enterprise identity provider.',
/** UNTRANSLATED */
finish_button_text: 'Continue',
},
basic_info: {
/** UNTRANSLATED */
title: 'Configure your service in the IdP',
/** UNTRANSLATED */
description:
'Create a new application integration by SAML 2.0 in your {{name}} identity provider. Then paste the following value to it.',
saml: {
/** UNTRANSLATED */
acs_url_field_name: 'Assertion consumer service URL (Reply URL)',
/** UNTRANSLATED */
audience_uri_field_name: 'Audience URI (SP Entity ID)',
},
oidc: {
/** UNTRANSLATED */
redirect_uri_field_name: 'Redirect URI (Callback URL)',
},
},
attribute_mapping: {
/** UNTRANSLATED */
title: 'Attribute mappings',
/** UNTRANSLATED */
description:
'`id` and `email` are required to sync user profile from IdP. Enter the following claim name and value in your IdP.',
/** UNTRANSLATED */
col_sp_claims: 'Claim name of Logto',
/** UNTRANSLATED */
col_idp_claims: 'Claim name of identity provider',
/** UNTRANSLATED */
idp_claim_tooltip: 'The claim name of the identity provider',
},
metadata: {
/** UNTRANSLATED */
title: 'Configure the IdP metadata',
/** UNTRANSLATED */
description: 'Configure the metadata from the identity provider',
/** UNTRANSLATED */
dropdown_trigger_text: 'Use another configuration method',
/** UNTRANSLATED */
dropdown_title: 'select your configuration method',
/** UNTRANSLATED */
metadata_format_url: 'Enter the metadata URL',
/** UNTRANSLATED */
metadata_format_xml: 'Upload the metadata XML file',
/** UNTRANSLATED */
metadata_format_manual: 'Enter metadata details manually',
saml: {
/** UNTRANSLATED */
metadata_url_field_name: 'Metadata URL',
/** UNTRANSLATED */
metadata_url_description:
'Dynamically fetch data from the metadata URL and keep certificate up to date.',
/** UNTRANSLATED */
metadata_xml_field_name: 'Metadata XML file',
/** UNTRANSLATED */
metadata_xml_uploader_text: 'Upload metadata XML file',
/** UNTRANSLATED */
sign_in_endpoint_field_name: 'Sign on URL',
/** UNTRANSLATED */
idp_entity_id_field_name: 'IdP entity ID (Issuer)',
/** UNTRANSLATED */
certificate_field_name: 'Signing certificate',
/** UNTRANSLATED */
certificate_placeholder: 'Copy and paste the x509 certificate',
},
oidc: {
/** UNTRANSLATED */
client_id_field_name: 'Client ID',
/** UNTRANSLATED */
client_secret_field_name: 'Client secret',
/** UNTRANSLATED */
issuer_field_name: 'Issuer',
/** UNTRANSLATED */
scope_field_name: 'Scope',
},
},
};
export default Object.freeze(enterprise_sso);

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { SsoConnectors } from '../db-entries/sso-connector.js';
import { SsoConnectors, type SsoConnector } from '../db-entries/sso-connector.js';
/**
* SSO Connector data type that are returned to the experience client for sign-in use.
@ -14,8 +14,17 @@ export const ssoConnectorMetadataGuard = z.object({
export type SsoConnectorMetadata = z.infer<typeof ssoConnectorMetadataGuard>;
export enum SsoProviderName {
OIDC = 'OIDC',
SAML = 'SAML',
}
export type SupportedSsoConnector = Omit<SsoConnector, 'providerName'> & {
providerName: SsoProviderName;
};
const ssoConnectorFactoryDetailGuard = z.object({
providerName: z.string(),
providerName: z.nativeEnum(SsoProviderName),
logo: z.string(),
description: z.string(),
});
@ -29,11 +38,15 @@ export const ssoConnectorFactoriesResponseGuard = z.object({
export type SsoConnectorFactoriesResponse = z.infer<typeof ssoConnectorFactoriesResponseGuard>;
export const ssoConnectorWithProviderConfigGuard = SsoConnectors.guard.merge(
z.object({
providerLogo: z.string(),
providerConfig: z.record(z.unknown()).optional(),
})
);
export const ssoConnectorWithProviderConfigGuard = SsoConnectors.guard
.omit({ providerName: true })
.merge(
z.object({
providerName: z.nativeEnum(SsoProviderName),
providerLogo: z.string(),
providerConfig: z.record(z.unknown()).optional(),
providerProperties: z.record(z.unknown()).optional(),
})
);
export type SsoConnectorWithProviderConfig = z.infer<typeof ssoConnectorWithProviderConfigGuard>;