0
Fork 0
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:
simeng-li 2023-11-28 11:34:48 +08:00 committed by GitHub
parent 66d6491d2e
commit b48ab51098
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 169 additions and 20 deletions

View file

@ -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,
},

View file

@ -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,
};
});
};
/**

View file

@ -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}
`);
}
}

View file

@ -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);

View file

@ -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',
},

View file

@ -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'],
});

View file

@ -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'],
});

View file

@ -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,

View file

@ -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'],
});

View file

@ -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,

View file

@ -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: [],
});

View file

@ -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 ? '+' : '';

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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(),
});