0
Fork 0
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:
simeng-li 2023-11-14 10:07:13 +08:00 committed by GitHub
parent e676f0c6eb
commit b277cb3b99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 280 additions and 2 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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

View file

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

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