mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
refactor(core,schemas): add SSO displayName (#4969)
feat(core,schemas): add SSO connector displayName property add SSO connector displayName property
This commit is contained in:
parent
66d6491d2e
commit
b48ab51098
28 changed files with 169 additions and 20 deletions
|
@ -159,7 +159,7 @@ describe('getFullSignInExperience()', () => {
|
|||
ssoConnectors: [
|
||||
{
|
||||
id: wellConfiguredSsoConnector.id,
|
||||
connectorName: wellConfiguredSsoConnector.connectorName,
|
||||
connectorName: wellConfiguredSsoConnector.providerName,
|
||||
logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo,
|
||||
darkLogo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logoDark,
|
||||
},
|
||||
|
@ -183,7 +183,34 @@ describe('get sso connectors', () => {
|
|||
expect(ssoConnectors).toEqual([
|
||||
{
|
||||
id: wellConfiguredSsoConnector.id,
|
||||
connectorName: wellConfiguredSsoConnector.connectorName,
|
||||
connectorName: wellConfiguredSsoConnector.providerName,
|
||||
logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo,
|
||||
darkLogo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logoDark,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return displayName if provided', async () => {
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockSocialConnectors);
|
||||
findDefaultSignInExperience.mockResolvedValueOnce(mockSignInExperience);
|
||||
|
||||
const displayName = 'Logto Connector';
|
||||
|
||||
ssoConnectorLibrary.getAvailableSsoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
...wellConfiguredSsoConnector,
|
||||
branding: {
|
||||
displayName,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { ssoConnectors } = await getFullSignInExperience();
|
||||
|
||||
expect(ssoConnectors).toEqual([
|
||||
{
|
||||
id: wellConfiguredSsoConnector.id,
|
||||
connectorName: displayName,
|
||||
logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo,
|
||||
darkLogo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logoDark,
|
||||
},
|
||||
|
|
|
@ -71,18 +71,17 @@ export const createSignInExperienceLibrary = (
|
|||
|
||||
const ssoConnectors = await getAvailableSsoConnectors();
|
||||
|
||||
return ssoConnectors.map(
|
||||
({ providerName, connectorName, id, branding }): SsoConnectorMetadata => {
|
||||
const factory = ssoConnectorFactories[providerName];
|
||||
return ssoConnectors.map(({ providerName, id, branding }): SsoConnectorMetadata => {
|
||||
const factory = ssoConnectorFactories[providerName];
|
||||
|
||||
return {
|
||||
id,
|
||||
connectorName,
|
||||
logo: branding.logo ?? factory.logo,
|
||||
darkLogo: branding.darkLogo ?? factory.logoDark,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
id,
|
||||
// Use the provider name as the connector name if the branding displayName is not provided
|
||||
connectorName: branding.displayName ?? providerName,
|
||||
logo: branding.logo ?? factory.logo,
|
||||
darkLogo: branding.darkLogo ?? factory.logoDark,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,8 @@ import {
|
|||
type SsoConnectorKeys,
|
||||
SsoConnectors,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||
|
||||
|
@ -16,4 +17,13 @@ export default class SsoConnectorQueries extends SchemaQueries<
|
|||
constructor(pool: CommonQueryMethods) {
|
||||
super(pool, SsoConnectors);
|
||||
}
|
||||
|
||||
async findByConnectorName(connectorName: string) {
|
||||
const { table, fields } = convertToIdentifiers(SsoConnectors);
|
||||
|
||||
return this.pool.maybeOne<SsoConnector>(sql`
|
||||
SELECT * FROM ${table}
|
||||
where ${fields.connectorName}=${connectorName}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso
|
|||
import { ssoConnectorFactories, standardSsoConnectorProviders } from '#src/sso/index.js';
|
||||
import { isSupportedSsoProvider, isSupportedSsoConnector } from '#src/sso/utils.js';
|
||||
import { tableToPathname } from '#src/utils/SchemaRouter.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
|
@ -109,6 +110,12 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
|
|||
// Validate the connector config if it's provided
|
||||
const parsedConfig = config && parseConnectorConfig(providerName, config);
|
||||
|
||||
// Validate the connector name is unique
|
||||
if (connectorName) {
|
||||
const duplicateConnector = await ssoConnectors.findByConnectorName(connectorName);
|
||||
assertThat(!duplicateConnector, 'single_sign_on.duplicate_connector_name');
|
||||
}
|
||||
|
||||
const connectorId = generateStandardShortId();
|
||||
|
||||
// Check the connection status of the connector config if it's provided
|
||||
|
@ -224,6 +231,12 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
|
|||
validateConnectorDomains(domains);
|
||||
}
|
||||
|
||||
// Validate the connector name is unique
|
||||
if (rest.connectorName) {
|
||||
const duplicateConnector = await ssoConnectors.findByConnectorName(rest.connectorName);
|
||||
assertThat(!duplicateConnector, 'single_sign_on.duplicate_connector_name');
|
||||
}
|
||||
|
||||
// Validate the connector config if it's provided
|
||||
const parsedConfig = config && parseConnectorConfig(providerName, config);
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export const newOidcSsoConnectorPayload = {
|
|||
connectorName: 'test-oidc',
|
||||
domains: ['example.io'], // Auto-generated email domain
|
||||
branding: {
|
||||
displayName: 'test oidc connector',
|
||||
logo: 'https://logto.io/oidc-logo.png',
|
||||
darkLogo: 'https://logto.io/oidc-dark-logo.png',
|
||||
},
|
||||
|
|
|
@ -21,7 +21,12 @@ import {
|
|||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import { generateEmail, generatePassword, generateUsername } from '#src/utils.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generatePassword,
|
||||
generateSsoConnectorName,
|
||||
generateUsername,
|
||||
} from '#src/utils.js';
|
||||
|
||||
describe('Register with identifiers sad path', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -59,6 +64,7 @@ describe('Register with identifiers sad path', () => {
|
|||
|
||||
await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
domains: ['sso-register-sad-path.io'],
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,12 @@ import {
|
|||
import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
|
||||
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser } from '#src/helpers/user.js';
|
||||
import { generateEmail, generatePassword, generatePhone } from '#src/utils.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generatePassword,
|
||||
generatePhone,
|
||||
generateSsoConnectorName,
|
||||
} from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow sad path using verification-code identifiers', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -220,6 +225,7 @@ describe('Sign-in flow sad path using verification-code identifiers', () => {
|
|||
|
||||
await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
domains: ['sso-sad-path.io'],
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import { generateSsoConnectorName } from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow using password identifiers', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -79,7 +80,10 @@ describe('Sign-in flow using password identifiers', () => {
|
|||
const client = await initClient();
|
||||
|
||||
// Create a new OIDC SSO connector with email domain 'example.com', it should not block the sign-in flow of email logto.io
|
||||
await createSsoConnector(newOidcSsoConnectorPayload);
|
||||
await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
});
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
|
|
|
@ -10,7 +10,12 @@ import { clearSsoConnectors } from '#src/helpers/connector.js';
|
|||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser } from '#src/helpers/user.js';
|
||||
import { generateEmail, generateName, generatePassword } from '#src/utils.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generateName,
|
||||
generatePassword,
|
||||
generateSsoConnectorName,
|
||||
} from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow sad path using password identifiers', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -191,6 +196,7 @@ describe('Sign-in flow sad path using password identifiers', () => {
|
|||
|
||||
await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
domains: ['sso-sad-path.io'],
|
||||
});
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
deleteSsoConnectorById,
|
||||
patchSsoConnectorById,
|
||||
} from '#src/api/sso-connector.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
||||
describe('sso-connector library', () => {
|
||||
it('should return sso-connector-factories', async () => {
|
||||
|
@ -57,6 +58,23 @@ describe('post sso-connectors', () => {
|
|||
).rejects.toThrow(HTTPError);
|
||||
});
|
||||
|
||||
it('should throw error when connectorName is not unique', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
providerName: 'OIDC',
|
||||
connectorName: 'test connector name',
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
createSsoConnector({
|
||||
providerName: 'OIDC',
|
||||
connectorName: 'test connector name',
|
||||
}),
|
||||
{ code: 'single_sign_on.duplicate_connector_name', statusCode: 400 }
|
||||
);
|
||||
|
||||
await deleteSsoConnectorById(id);
|
||||
});
|
||||
|
||||
it.each(providerNames)('should create a new sso connector', async (providerName) => {
|
||||
const response = await createSsoConnector({
|
||||
providerName,
|
||||
|
@ -174,6 +192,28 @@ describe('patch sso-connector by id', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should throw error if connector name is not unique', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
providerName: 'OIDC',
|
||||
connectorName: 'test connector name',
|
||||
});
|
||||
|
||||
const { id: id2 } = await createSsoConnector({
|
||||
providerName: 'OIDC',
|
||||
connectorName: 'test connector name 2',
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
patchSsoConnectorById(id2, {
|
||||
connectorName: 'test connector name',
|
||||
}),
|
||||
{ code: 'single_sign_on.duplicate_connector_name', statusCode: 400 }
|
||||
);
|
||||
|
||||
await deleteSsoConnectorById(id);
|
||||
await deleteSsoConnectorById(id2);
|
||||
});
|
||||
|
||||
it.each(providerNames)('should patch sso connector without config', async (providerName) => {
|
||||
const { id } = await createSsoConnector({
|
||||
providerName,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { HTTPError } from 'got';
|
|||
import api, { adminTenantApi, authedAdminApi } from '#src/api/api.js';
|
||||
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
|
||||
import { newOidcSsoConnectorPayload } from '#src/constants.js';
|
||||
import { generateSsoConnectorName } from '#src/utils.js';
|
||||
|
||||
describe('.well-known api', () => {
|
||||
it('should return tenant endpoint URL for any given tenant id', async () => {
|
||||
|
@ -57,7 +58,10 @@ describe('.well-known api', () => {
|
|||
|
||||
describe('sso connectors in sign-in experience', () => {
|
||||
it('should get the sso connectors in sign-in experience', async () => {
|
||||
const { id, connectorName } = await createSsoConnector(newOidcSsoConnectorPayload);
|
||||
const { id } = await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
});
|
||||
|
||||
const signInExperience = await api
|
||||
.get('.well-known/sign-in-exp')
|
||||
|
@ -71,7 +75,7 @@ describe('.well-known api', () => {
|
|||
|
||||
expect(newCreatedConnector).toMatchObject({
|
||||
id,
|
||||
connectorName,
|
||||
connectorName: newOidcSsoConnectorPayload.branding.displayName,
|
||||
logo: newOidcSsoConnectorPayload.branding.logo,
|
||||
darkLogo: newOidcSsoConnectorPayload.branding.darkLogo,
|
||||
});
|
||||
|
@ -82,6 +86,7 @@ describe('.well-known api', () => {
|
|||
it('should filter out the sso connectors with invalid config', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
config: undefined,
|
||||
});
|
||||
|
||||
|
@ -99,6 +104,7 @@ describe('.well-known api', () => {
|
|||
it('should filter out the sso connectors with empty domains', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
...newOidcSsoConnectorPayload,
|
||||
connectorName: generateSsoConnectorName(),
|
||||
domains: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ export const generateEmail = (domain = 'logto.io') =>
|
|||
export const generateScopeName = () => `sc:${crypto.randomUUID()}`;
|
||||
export const generateRoleName = () => `role_${crypto.randomUUID()}`;
|
||||
export const generateDomain = () => `${crypto.randomUUID().toLowerCase().slice(0, 5)}.example.com`;
|
||||
export const generateSsoConnectorName = () => `sso_${crypto.randomUUID()}`;
|
||||
|
||||
export const generatePhone = (isE164?: boolean) => {
|
||||
const plus = isE164 ? '+' : '';
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Es gibt doppelte Domänen.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,7 @@ const single_sign_on = {
|
|||
duplicated_domains: 'There are duplicate domains.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Hay dominios duplicados.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Il existe des domaines en double.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Ci sono domini duplicati.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: '重複するドメインがあります。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: '중복된 도메인이 있습니다.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Istnieją zduplikowane domeny.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Existem domínios duplicados.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Existem domínios duplicados.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Есть дублирующиеся домены.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: 'Yinelenmiş domainler bulunmaktadır.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: '存在重复的域名。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: '存在重複的域名。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -3,6 +3,8 @@ const single_sign_on = {
|
|||
duplicated_domains: '存在重複的域名。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_domain_format: 'Invalid domain format.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_connector_name: 'Connector name already exists. Please choose a different name.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
||||
|
|
|
@ -5,6 +5,7 @@ export const ssoDomainsGuard = z.array(z.string());
|
|||
export type SsoDomains = z.infer<typeof ssoDomainsGuard>;
|
||||
|
||||
export const ssoBrandingGuard = z.object({
|
||||
displayName: z.string().optional(),
|
||||
logo: z.string().optional(),
|
||||
darkLogo: z.string().optional(),
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue