mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(core,phrases): add sso domains validation (#4855)
* feat(core,phrases): add sso domains validation add sso domains validation * chore(core): align the naming align the naming
This commit is contained in:
parent
e676f0c6eb
commit
b277cb3b99
34 changed files with 280 additions and 2 deletions
|
@ -22,6 +22,7 @@ import {
|
|||
parseFactoryDetail,
|
||||
parseConnectorConfig,
|
||||
fetchConnectorProviderDetails,
|
||||
validateConnectorDomains,
|
||||
} from './utils.js';
|
||||
|
||||
export default function singleSignOnRoutes<T extends AuthedRouter>(...args: RouterInitArgs<T>) {
|
||||
|
@ -93,6 +94,9 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
|
|||
});
|
||||
}
|
||||
|
||||
// Validate the connector domains if it's provided
|
||||
validateConnectorDomains(rest.domains);
|
||||
|
||||
/*
|
||||
Validate the connector config if it's provided.
|
||||
Allow partial config settings on create.
|
||||
|
@ -195,6 +199,9 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
|
|||
const { providerName } = originalConnector;
|
||||
const { config, ...rest } = body;
|
||||
|
||||
// Validate the connector domains if it's provided
|
||||
validateConnectorDomains(rest.domains);
|
||||
|
||||
// Validate the connector config if it's provided
|
||||
const parsedConfig = config && parseConnectorConfig(providerName, config);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
@ -12,7 +13,8 @@ await mockEsmWithActual('#src/sso/OidcConnector/utils.js', () => ({
|
|||
}));
|
||||
|
||||
const { ssoConnectorFactories } = await import('#src/sso/index.js');
|
||||
const { parseFactoryDetail, fetchConnectorProviderDetails } = await import('./utils.js');
|
||||
const { parseFactoryDetail, fetchConnectorProviderDetails, validateConnectorDomains } =
|
||||
await import('./utils.js');
|
||||
|
||||
const mockTenantId = 'mock_tenant_id';
|
||||
|
||||
|
@ -99,3 +101,43 @@ describe('fetchConnectorProviderDetails', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConnectorDomains', () => {
|
||||
it('should directly return if domains are not provided', () => {
|
||||
expect(() => {
|
||||
validateConnectorDomains();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should directly return if domains are empty', () => {
|
||||
expect(() => {
|
||||
validateConnectorDomains([]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error if domains are duplicated', () => {
|
||||
expect(() => {
|
||||
validateConnectorDomains(['foo.io', 'bar.io', 'foo.io']);
|
||||
}).toMatchError(
|
||||
new RequestError(
|
||||
{ code: 'single_sign_on.duplicated_domains', status: 422 },
|
||||
{
|
||||
data: ['foo.io'],
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if domains are in the blacklist', () => {
|
||||
expect(() => {
|
||||
validateConnectorDomains(['foo.io', 'bar.io', 'gmail.com']);
|
||||
}).toMatchError(
|
||||
new RequestError(
|
||||
{ code: 'single_sign_on.forbidden_domains', status: 422 },
|
||||
{
|
||||
data: ['gmail.com'],
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,11 @@ import { type JsonObject, type SsoConnectorWithProviderConfig } from '@logto/sch
|
|||
import { conditional, trySafe } from '@silverhand/essentials';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type SingleSignOnFactory, ssoConnectorFactories } from '#src/sso/index.js';
|
||||
import {
|
||||
type SingleSignOnFactory,
|
||||
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 =>
|
||||
|
@ -73,3 +77,58 @@ export const fetchConnectorProviderDetails = async (
|
|||
...conditional(providerConfig && { providerConfig }),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the connector domains using the domain blacklist.
|
||||
* - Throw error if the domains are invalid.
|
||||
* - Throw error if the domains are duplicated.
|
||||
*
|
||||
* @param domains
|
||||
* @returns
|
||||
*/
|
||||
export const validateConnectorDomains = (domains?: string[]) => {
|
||||
if (!domains || domains.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blackListSet = new Set(singleSignOnDomainBlackList);
|
||||
const validDomainSet = new Set();
|
||||
const duplicatedDomains = new Set();
|
||||
const forbiddenDomains = new Set();
|
||||
|
||||
for (const domain of domains) {
|
||||
if (blackListSet.has(domain)) {
|
||||
forbiddenDomains.add(domain);
|
||||
}
|
||||
|
||||
if (validDomainSet.has(domain)) {
|
||||
duplicatedDomains.add(domain);
|
||||
} else {
|
||||
validDomainSet.add(domain);
|
||||
}
|
||||
}
|
||||
|
||||
if (forbiddenDomains.size > 0) {
|
||||
throw new RequestError(
|
||||
{
|
||||
code: 'single_sign_on.forbidden_domains',
|
||||
status: 422,
|
||||
},
|
||||
{
|
||||
data: [...forbiddenDomains],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (duplicatedDomains.size > 0) {
|
||||
throw new RequestError(
|
||||
{
|
||||
code: 'single_sign_on.duplicated_domains',
|
||||
status: 422,
|
||||
},
|
||||
{
|
||||
data: [...duplicatedDomains],
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -38,3 +38,25 @@ export const standardSsoConnectorProviders = Object.freeze([
|
|||
SsoProviderName.OIDC,
|
||||
SsoProviderName.SAML,
|
||||
]);
|
||||
|
||||
export const singleSignOnDomainBlackList = Object.freeze([
|
||||
'gmail.com',
|
||||
'yahoo.com',
|
||||
'hotmail.com',
|
||||
'outlook.com',
|
||||
'live.com',
|
||||
'icloud.com',
|
||||
'aol.com',
|
||||
'yandex.com',
|
||||
'mail.com',
|
||||
'protonmail.com',
|
||||
'yanex.com',
|
||||
'gmx.com',
|
||||
'mail.ru',
|
||||
'zoho.com',
|
||||
'qq.com',
|
||||
'163.com',
|
||||
'126.com',
|
||||
'sina.com',
|
||||
'sohu.com',
|
||||
]);
|
||||
|
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/de/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/de/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/en/errors/single-sign-on.ts
Normal file
6
packages/phrases/src/locales/en/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const single_sign_on = {
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/es/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/es/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/fr/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/fr/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/it/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/it/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/ja/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/ja/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/ko/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/ko/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
8
packages/phrases/src/locales/ru/errors/single-sign-on.ts
Normal file
8
packages/phrases/src/locales/ru/errors/single-sign-on.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
|
@ -16,6 +16,7 @@ import role from './role.js';
|
|||
import scope from './scope.js';
|
||||
import session from './session.js';
|
||||
import sign_in_experiences from './sign-in-experiences.js';
|
||||
import single_sign_on from './single-sign-on.js';
|
||||
import storage from './storage.js';
|
||||
import subscription from './subscription.js';
|
||||
import swagger from './swagger.js';
|
||||
|
@ -46,6 +47,7 @@ const errors = {
|
|||
subscription,
|
||||
application,
|
||||
organization,
|
||||
single_sign_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const single_sign_on = {
|
||||
/** UNTRANSLATED */
|
||||
forbidden_domains: 'Public email domains are not allowed.',
|
||||
/** UNTRANSLATED */
|
||||
duplicated_domains: 'There are duplicate domains.',
|
||||
};
|
||||
|
||||
export default Object.freeze(single_sign_on);
|
Loading…
Add table
Reference in a new issue