mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
fix(console,phrases,core): fix SAML attribute mapping (#4978)
This commit is contained in:
parent
2c038b25ed
commit
fad8872b31
25 changed files with 112 additions and 111 deletions
|
@ -5,21 +5,15 @@
|
|||
*/
|
||||
import { type SsoProviderName, type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
|
||||
export type AttributeMapping = {
|
||||
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 const attributeKeys = Object.freeze(['id', 'email', 'name']) satisfies ReadonlyArray<
|
||||
keyof AttributeMapping
|
||||
>;
|
||||
|
||||
export type SamlGuideFormType = {
|
||||
metadataUrl?: string;
|
||||
|
@ -60,6 +54,7 @@ export type ParsedSsoIdentityProviderConfig<T extends SsoProviderName> =
|
|||
}
|
||||
: T extends SsoProviderName.SAML
|
||||
? {
|
||||
defaultAttributeMapping: AttributeMapping;
|
||||
serviceProvider: {
|
||||
entityId: string;
|
||||
assertionConsumerServiceUrl: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { SsoProviderName, type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import { useContext } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -51,8 +51,8 @@ function BasicInfo({ ssoConnectorId, providerName, providerConfig }: Props) {
|
|||
const result = providerPropertiesGuard.safeParse(providerConfig);
|
||||
|
||||
/**
|
||||
* Used fallback to show the default value anyways but the cases should not happen.
|
||||
* TODO: @darcyYe refactor to remove the manual guard.
|
||||
* Should not fallback to some other manually concatenated URL, show empty string instead.
|
||||
* Empty string should never show up unless the API does not work properly.
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
|
@ -60,10 +60,10 @@ function BasicInfo({ ssoConnectorId, providerName, providerConfig }: Props) {
|
|||
<CopyToClipboard
|
||||
className={styles.copyToClipboard}
|
||||
variant="border"
|
||||
value={applyCustomDomain(
|
||||
conditional(
|
||||
result.success && result.data.serviceProvider.assertionConsumerServiceUrl
|
||||
) ?? new URL(`/api/authn/saml/sso/${ssoConnectorId}`, tenantEndpoint).toString()
|
||||
value={conditionalString(
|
||||
result.success &&
|
||||
result.data.serviceProvider.assertionConsumerServiceUrl &&
|
||||
applyCustomDomain(result.data.serviceProvider.assertionConsumerServiceUrl)
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
|
@ -71,9 +71,10 @@ function BasicInfo({ ssoConnectorId, providerName, providerConfig }: Props) {
|
|||
<CopyToClipboard
|
||||
className={styles.copyToClipboard}
|
||||
variant="border"
|
||||
value={applyCustomDomain(
|
||||
conditional(result.success && result.data.serviceProvider.entityId) ??
|
||||
new URL(`/api/sso-connector/${ssoConnectorId}`, tenantEndpoint).toString()
|
||||
value={conditionalString(
|
||||
result.success &&
|
||||
result.data.serviceProvider.entityId &&
|
||||
applyCustomDomain(result.data.serviceProvider.entityId)
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -23,13 +23,14 @@
|
|||
|
||||
.header {
|
||||
width: 100%;
|
||||
font: var(--font-title-3);
|
||||
|
||||
> tr {
|
||||
padding-bottom: _.unit(3);
|
||||
}
|
||||
|
||||
* > th {
|
||||
font: var(--font-label-2);
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,39 @@
|
|||
import { useMemo } from 'react';
|
||||
import { socialUserInfoGuard } from '@logto/connector-kit';
|
||||
import { type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
|
||||
import {
|
||||
type SamlGuideFormType,
|
||||
type AttributeMapping,
|
||||
attributeKeys,
|
||||
} from '../../../EnterpriseSso/types.js';
|
||||
import { attributeKeys, type SamlGuideFormType } from '../../../EnterpriseSso/types.js';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
type Props = Pick<SsoConnectorWithProviderConfig, 'providerConfig'>;
|
||||
|
||||
/**
|
||||
* TODO: Should align this with the guard `samlAttributeMappingGuard` defined in {@link logto/core/src/sso/types/saml.ts}.
|
||||
* This only applies to SAML-protocol-based SSO connectors.
|
||||
*/
|
||||
const providerPropertiesGuard = z.object({
|
||||
defaultAttributeMapping: socialUserInfoGuard
|
||||
.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
const primaryKey = 'attributeMapping';
|
||||
|
||||
function SamlAttributeMapping({ isReadOnly }: Props) {
|
||||
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]>>(
|
||||
() => attributeKeys.map((key) => [key, attributeMapping[key] ?? '']),
|
||||
[attributeMapping]
|
||||
);
|
||||
function SamlAttributeMapping({ providerConfig }: Props) {
|
||||
const { register } = useFormContext<SamlGuideFormType>();
|
||||
|
||||
const result = providerPropertiesGuard.safeParse(providerConfig);
|
||||
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
|
@ -41,22 +48,19 @@ function SamlAttributeMapping({ isReadOnly }: Props) {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody className={styles.body}>
|
||||
{attributeMappingEntries.map(([key, value]) => {
|
||||
{attributeKeys.map((key) => {
|
||||
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} />
|
||||
)}
|
||||
<TextInput
|
||||
{...register(`${primaryKey}.${key}`)}
|
||||
placeholder={conditional(
|
||||
result.success && result.data.defaultAttributeMapping[key]
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -132,8 +132,11 @@ function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: P
|
|||
<FormCard
|
||||
title="enterprise_sso_details.attribute_mapping_title"
|
||||
description="enterprise_sso_details.attribute_mapping_description"
|
||||
descriptionInterpolation={{
|
||||
name: ssoConnectorName,
|
||||
}}
|
||||
>
|
||||
<SamlAttributeMapping />
|
||||
<SamlAttributeMapping providerConfig={providerConfig} />
|
||||
</FormCard>
|
||||
)}
|
||||
</DetailsForm>
|
||||
|
|
|
@ -21,10 +21,10 @@ import {
|
|||
parseXmlMetadata,
|
||||
fetchSamlMetadataXml,
|
||||
handleSamlAssertion,
|
||||
attributeMappingPostProcessor,
|
||||
getExtendedUserInfoFromRawUserProfile,
|
||||
buildSpEntityId,
|
||||
buildAssertionConsumerServiceUrl,
|
||||
attributeMappingPostProcessor,
|
||||
} from './utils.js';
|
||||
|
||||
/**
|
||||
|
@ -127,6 +127,7 @@ class SamlConnector {
|
|||
const rawUserProfile = rawProfileParseResult.data;
|
||||
|
||||
const profileMap = attributeMappingPostProcessor(this.idpConfig.attributeMapping);
|
||||
|
||||
return getExtendedUserInfoFromRawUserProfile(rawUserProfile, profileMap);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { attributeMappingPostProcessor, getExtendedUserInfoFromRawUserProfile } from './utils.js';
|
||||
|
||||
const expectedDefaultAttributeMapping = {
|
||||
id: 'id',
|
||||
id: 'nameID',
|
||||
email: 'email',
|
||||
phone: 'phone',
|
||||
name: 'name',
|
||||
avatar: 'avatar',
|
||||
};
|
||||
|
||||
describe('attributeMappingPostProcessor', () => {
|
||||
|
@ -15,10 +13,9 @@ describe('attributeMappingPostProcessor', () => {
|
|||
});
|
||||
|
||||
it('should overwrite specified attributes of `expectedDefaultAttributeMapping`', () => {
|
||||
expect(attributeMappingPostProcessor({ id: 'sub', avatar: 'picture' })).toEqual({
|
||||
expect(attributeMappingPostProcessor({ id: 'sub' })).toEqual({
|
||||
...expectedDefaultAttributeMapping,
|
||||
id: 'sub',
|
||||
avatar: 'picture',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -46,24 +43,4 @@ describe('getExtendedUserInfoFromRawUserProfile', () => {
|
|||
avatar: 'pic.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly map with specific fields specified and with extended fields unchanged', () => {
|
||||
const keyMapping = attributeMappingPostProcessor({ phone: 'cell_phone', avatar: 'picture' });
|
||||
const rawUserProfile = {
|
||||
id: 'foo',
|
||||
sub: 'bar',
|
||||
email: 'test@logto.io',
|
||||
cell_phone: '123456789',
|
||||
picture: 'pic.png',
|
||||
extend_field: 'extend_field',
|
||||
};
|
||||
expect(getExtendedUserInfoFromRawUserProfile(rawUserProfile, keyMapping)).toEqual({
|
||||
id: 'foo',
|
||||
sub: 'bar',
|
||||
email: 'test@logto.io',
|
||||
phone: '123456789',
|
||||
avatar: 'pic.png',
|
||||
extend_field: 'extend_field',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as validator from '@authenio/samlify-node-xmllint';
|
||||
import { type Optional, conditional, appendPath } from '@silverhand/essentials';
|
||||
import { type Optional, appendPath } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { HTTPError, got } from 'got';
|
||||
import * as saml from 'samlify';
|
||||
import { z } from 'zod';
|
||||
|
@ -13,11 +14,11 @@ import {
|
|||
} from '../types/error.js';
|
||||
import {
|
||||
defaultAttributeMapping,
|
||||
type CustomizableAttributeMap,
|
||||
type AttributeMap,
|
||||
extendedSocialUserInfoGuard,
|
||||
type AttributeMap,
|
||||
type ExtendedSocialUserInfo,
|
||||
type SamlIdentityProviderMetadata,
|
||||
type CustomizableAttributeMap,
|
||||
samlIdentityProviderMetadataGuard,
|
||||
} from '../types/saml.js';
|
||||
|
||||
|
@ -107,15 +108,16 @@ export const fetchSamlMetadataXml = async (metadataUrl: string): Promise<Optiona
|
|||
* Get the user info from the raw user profile extracted from IdP SAML assertion.
|
||||
*
|
||||
* @param rawUserProfile The raw user profile extracted from IdP SAML assertion.
|
||||
* @param keyMapping The full attribute mapping with default values.
|
||||
* @param attributeMapping The full attribute mapping with default values.
|
||||
*
|
||||
* @returns The mapped social user info.
|
||||
*/
|
||||
export const getExtendedUserInfoFromRawUserProfile = (
|
||||
rawUserProfile: Record<string, unknown>,
|
||||
keyMapping: AttributeMap
|
||||
attributeMapping: AttributeMap
|
||||
): ExtendedSocialUserInfo => {
|
||||
const keyMap = new Map(
|
||||
Object.entries(keyMapping).map(([destination, source]) => [source, destination])
|
||||
Object.entries(attributeMapping).map(([destination, source]) => [source, destination])
|
||||
);
|
||||
|
||||
const mappedUserProfile = Object.fromEntries(
|
||||
|
@ -169,7 +171,8 @@ export const handleSamlAssertion = async (
|
|||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...(Boolean(assertionResult.extract.nameID) && {
|
||||
id: assertionResult.extract.nameID,
|
||||
// Usually identity provider DOES NOT allow the configuration of `nameID` claim name.
|
||||
nameID: assertionResult.extract.nameID,
|
||||
}),
|
||||
...assertionResult.extract.attributes,
|
||||
};
|
||||
|
|
|
@ -7,7 +7,11 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
import SamlConnector from '../SamlConnector/index.js';
|
||||
import { type SingleSignOnFactory } from '../index.js';
|
||||
import { type SingleSignOn, type SingleSignOnConnectorData } from '../types/index.js';
|
||||
import { samlConnectorConfigGuard, type SamlMetadata } from '../types/saml.js';
|
||||
import {
|
||||
defaultAttributeMapping,
|
||||
samlConnectorConfigGuard,
|
||||
type SamlMetadata,
|
||||
} from '../types/saml.js';
|
||||
import {
|
||||
type SingleSignOnConnectorSession,
|
||||
type CreateSingleSignOnSession,
|
||||
|
@ -43,14 +47,16 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
|
|||
}
|
||||
|
||||
/**
|
||||
* ServiceProvider: SP metadata
|
||||
* identityProvider: IdP metadata. Returns undefined if the idp config is invalid.
|
||||
* `defaultAttributeMapping`: Default attribute mapping
|
||||
* `serviceProvider`: SP metadata
|
||||
* `identityProvider`: IdP metadata. Returns undefined if the idp config is invalid.
|
||||
*/
|
||||
async getConfig(): Promise<SamlMetadata> {
|
||||
const serviceProvider = this.serviceProviderMetadata;
|
||||
const identityProvider = await trySafe(async () => this.getSamlIdpMetadata());
|
||||
|
||||
return {
|
||||
defaultAttributeMapping,
|
||||
serviceProvider,
|
||||
identityProvider,
|
||||
};
|
||||
|
|
|
@ -2,16 +2,25 @@ import { socialUserInfoGuard } from '@logto/connector-kit';
|
|||
import { z } from 'zod';
|
||||
|
||||
// Since the SAML SSO user info will extend the basic social user info (will contain extra info like `organization`, `role` etc.), but for now we haven't decide what should be included in extended user info, so we just use the basic social user info guard here to keep SSOT.
|
||||
const samlAttributeMappingGuard = socialUserInfoGuard;
|
||||
const samlAttributeMappingGuard = socialUserInfoGuard
|
||||
.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const defaultAttributeMapping = Object.fromEntries(
|
||||
Object.keys(samlAttributeMappingGuard.shape).map((key) => [key, key])
|
||||
) as AttributeMap;
|
||||
export const defaultAttributeMapping: AttributeMap = {
|
||||
id: 'nameID',
|
||||
email: 'email',
|
||||
name: 'name',
|
||||
};
|
||||
|
||||
const customizableAttributeMappingGuard = samlAttributeMappingGuard.partial();
|
||||
export type CustomizableAttributeMap = z.infer<typeof customizableAttributeMappingGuard>;
|
||||
export type AttributeMap = Required<CustomizableAttributeMap>;
|
||||
const customizableAttributeMapGuard = samlAttributeMappingGuard.partial();
|
||||
|
||||
export type CustomizableAttributeMap = z.infer<typeof customizableAttributeMapGuard>;
|
||||
|
||||
export type AttributeMap = z.infer<typeof samlAttributeMappingGuard>;
|
||||
|
||||
/**
|
||||
* This is the metadata of SAML service provider, automatically generated by `tenantId` and `ssoConnectorId`.
|
||||
|
@ -38,21 +47,22 @@ export const samlConnectorConfigGuard = z.union([
|
|||
// Config using Metadata URL
|
||||
z.object({
|
||||
metadataUrl: z.string(),
|
||||
attributeMapping: customizableAttributeMappingGuard.optional(),
|
||||
attributeMapping: customizableAttributeMapGuard.optional(),
|
||||
}),
|
||||
// Config using Metadata XML
|
||||
z.object({
|
||||
metadata: z.string(),
|
||||
attributeMapping: customizableAttributeMappingGuard.optional(),
|
||||
attributeMapping: customizableAttributeMapGuard.optional(),
|
||||
}),
|
||||
// Config using Metadata detail
|
||||
samlIdentityProviderMetadataGuard.extend({
|
||||
attributeMapping: customizableAttributeMappingGuard.optional(),
|
||||
attributeMapping: customizableAttributeMapGuard.optional(),
|
||||
}),
|
||||
]);
|
||||
export type SamlConnectorConfig = z.infer<typeof samlConnectorConfigGuard>;
|
||||
|
||||
const samlMetadataGuard = z.object({
|
||||
defaultAttributeMapping: samlAttributeMappingGuard,
|
||||
serviceProvider: samlServiceProviderMetadataGuard,
|
||||
identityProvider: samlIdentityProviderMetadataGuard.optional(),
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -37,7 +37,7 @@ const enterprise_sso = {
|
|||
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_sp_claims: 'Value of service provider (Logto)',
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
idp_claim_tooltip: 'The claim name of the identity provider',
|
||||
},
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -49,7 +49,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -47,7 +47,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -48,7 +48,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -47,7 +47,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -47,7 +47,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -47,7 +47,7 @@ const enterprise_sso = {
|
|||
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',
|
||||
col_sp_claims: 'Value of service provider (Logto)',
|
||||
/** UNTRANSLATED */
|
||||
col_idp_claims: 'Claim name of identity provider',
|
||||
/** UNTRANSLATED */
|
||||
|
|
Loading…
Add table
Reference in a new issue