mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
fix(core): fix SAML app redirect URIs sync custom domains logic (#7047)
fix: fix SAML app redirect URIs sync custom domains logic
This commit is contained in:
parent
28643c1f1a
commit
5735d9ddfc
3 changed files with 63 additions and 70 deletions
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { SearchJointMode } from '@logto/schemas';
|
import { SearchJointMode } from '@logto/schemas';
|
||||||
import { generateStandardId, ConsoleLog } from '@logto/shared';
|
import { generateStandardId, ConsoleLog } from '@logto/shared';
|
||||||
import { removeUndefinedKeys, pick } from '@silverhand/essentials';
|
import { removeUndefinedKeys, pick, deduplicate } from '@silverhand/essentials';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||||
|
@ -123,19 +123,19 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
|
||||||
* Applies custom domain configuration to SAML application redirect URIs.
|
* Applies custom domain configuration to SAML application redirect URIs.
|
||||||
*
|
*
|
||||||
* @param currentTenantId - The ID of the current tenant used for constructing default hostname URIs
|
* @param currentTenantId - The ID of the current tenant used for constructing default hostname URIs
|
||||||
* @param domain - Current tenant custom domain status, if there is no custom domain, pass `undefined`
|
* @param domains - Current tenant custom domain status, if there is no custom domain, pass `undefined`
|
||||||
* @returns
|
* @returns
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // With active domain
|
* // With active domain
|
||||||
* const app = { redirectUris: ['https://original.example.com'] };
|
* const app = { redirectUris: ['https://original.example.com'] };
|
||||||
* syncCustomDomainToSamlApplicationRedirectUrls('tenant1', { domain: 'https://custom.domain.com' });
|
* syncCustomDomainsToSamlApplicationRedirectUrls('tenant1', { domain: 'https://custom.domain.com' });
|
||||||
* // redirectUris becomes ['https://original.example.com', 'https://custom.domain.com']
|
* // redirectUris becomes ['https://original.example.com', 'https://custom.domain.com']
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Without active domain
|
* // Without active domain
|
||||||
* const app = { redirectUris: ['https://original.example.com', 'https://custom.domain.com'] };
|
* const app = { redirectUris: ['https://original.example.com', 'https://custom.domain.com'] };
|
||||||
* syncCustomDomainToSamlApplicationRedirectUrls('tenant1');
|
* syncCustomDomainsToSamlApplicationRedirectUrls('tenant1');
|
||||||
* // redirectUris becomes ['https://original.example.com']
|
* // redirectUris becomes ['https://original.example.com']
|
||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
|
@ -145,9 +145,9 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
|
||||||
* Ref:
|
* Ref:
|
||||||
* - SAML application: https://github.com/logto-io/rfcs-internal/pull/5
|
* - SAML application: https://github.com/logto-io/rfcs-internal/pull/5
|
||||||
*/
|
*/
|
||||||
const syncCustomDomainToSamlApplicationRedirectUrls = async (
|
const syncCustomDomainsToSamlApplicationRedirectUrls = async (
|
||||||
currentTenantId: string,
|
currentTenantId: string,
|
||||||
domain?: Domain
|
domains: Domain[]
|
||||||
) => {
|
) => {
|
||||||
// Skip for admin tenant and non-cloud environment.
|
// Skip for admin tenant and non-cloud environment.
|
||||||
if (currentTenantId === adminTenantId || !EnvSet.values.isCloud) {
|
if (currentTenantId === adminTenantId || !EnvSet.values.isCloud) {
|
||||||
|
@ -161,68 +161,53 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
|
||||||
types: [ApplicationType.SAML],
|
types: [ApplicationType.SAML],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply custom domain to SAML applications' redirect URIs if the custom domain is active.
|
const applyCustomDomain = (url: string, domain: Domain): string => {
|
||||||
// eslint-disable-next-line unicorn/prefer-ternary
|
const parsedUrl = new URL(url);
|
||||||
if (domain?.status === DomainStatus.Active) {
|
|
||||||
await Promise.all(
|
// Apply custom domain to SAML applications' redirect URIs if the custom domain is active.
|
||||||
samlApplications.map(async (samlApplication) => {
|
if (domain.status === DomainStatus.Active) {
|
||||||
// There is only one default redirect URI for each SAML app.
|
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||||
const defaultRedirectUri = samlApplication.oidcClientMetadata.redirectUris.find((url) =>
|
parsedUrl.hostname = domain.domain;
|
||||||
url.startsWith(getTenantEndpoint(currentTenantId, EnvSet.values).hostname)
|
}
|
||||||
|
|
||||||
|
return parsedUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
samlApplications.map(async (samlApplication) => {
|
||||||
|
const defaultRedirectUri = samlApplication.oidcClientMetadata.redirectUris.find((url) =>
|
||||||
|
url.startsWith(getTenantEndpoint(currentTenantId, EnvSet.values).toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not happen.
|
||||||
|
if (!defaultRedirectUri) {
|
||||||
|
consoleLog.warn(
|
||||||
|
`Can not apply custom domain to SAML app ${samlApplication.id}, since we can not find default redirect URI.`
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Should not happen.
|
const newRedirectUris = deduplicate([
|
||||||
if (!defaultRedirectUri) {
|
defaultRedirectUri,
|
||||||
consoleLog.warn(
|
// If the custom domain is deleted or not active, we should remove all custom domains from the redirect URIs.
|
||||||
`Can not apply custom domain to SAML app ${samlApplication.id}, since we can not find default redirect URI.`
|
...domains.map((domain) => applyCustomDomain(defaultRedirectUri, domain)),
|
||||||
);
|
]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectUriWithCustomDomain = new URL(defaultRedirectUri);
|
const updatedApplication = await updateApplicationById(samlApplication.id, {
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
oidcClientMetadata: {
|
||||||
redirectUriWithCustomDomain.hostname = domain.domain;
|
...samlApplication.oidcClientMetadata,
|
||||||
|
redirectUris: newRedirectUris,
|
||||||
await updateApplicationById(samlApplication.id, {
|
},
|
||||||
oidcClientMetadata: {
|
});
|
||||||
...samlApplication.oidcClientMetadata,
|
consoleLog.info('updatedApplication', updatedApplication);
|
||||||
redirectUris: [defaultRedirectUri, redirectUriWithCustomDomain.toString()],
|
})
|
||||||
},
|
);
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// If the custom domain is deleted or not active, we should remove all custom domains from the redirect URIs.
|
|
||||||
await Promise.all(
|
|
||||||
samlApplications.map(async (samlApplication) => {
|
|
||||||
const redirectUrisWithoutCustomDomains =
|
|
||||||
// Filter out redirect URIs applied with custom domain.
|
|
||||||
samlApplication.oidcClientMetadata.redirectUris.filter((url) =>
|
|
||||||
url.startsWith(getTenantEndpoint(currentTenantId, EnvSet.values).hostname)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (redirectUrisWithoutCustomDomains.length !== 1) {
|
|
||||||
consoleLog.warn(
|
|
||||||
`Can not apply custom domain to SAML app ${samlApplication.id}, the redirect URIs do not match the current tenant's endpoint.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateApplicationById(samlApplication.id, {
|
|
||||||
oidcClientMetadata: {
|
|
||||||
...samlApplication.oidcClientMetadata,
|
|
||||||
redirectUris: redirectUrisWithoutCustomDomains,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createSamlApplicationSecret,
|
createSamlApplicationSecret,
|
||||||
findSamlApplicationById,
|
findSamlApplicationById,
|
||||||
updateSamlApplicationById,
|
updateSamlApplicationById,
|
||||||
syncCustomDomainToSamlApplicationRedirectUrls,
|
syncCustomDomainsToSamlApplicationRedirectUrls,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@ const mockLibraries = {
|
||||||
},
|
},
|
||||||
quota: createMockQuotaLibrary(),
|
quota: createMockQuotaLibrary(),
|
||||||
samlApplications: {
|
samlApplications: {
|
||||||
syncCustomDomainToSamlApplicationRedirectUrls: jest.fn(),
|
syncCustomDomainsToSamlApplicationRedirectUrls: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function domainRoutes<T extends ManagementApiRouter>(
|
||||||
} = queries;
|
} = queries;
|
||||||
const {
|
const {
|
||||||
domains: { syncDomainStatus, addDomain, deleteDomain },
|
domains: { syncDomainStatus, addDomain, deleteDomain },
|
||||||
samlApplications: { syncCustomDomainToSamlApplicationRedirectUrls },
|
samlApplications: { syncCustomDomainsToSamlApplicationRedirectUrls },
|
||||||
} = libraries;
|
} = libraries;
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -28,12 +28,8 @@ export default function domainRoutes<T extends ManagementApiRouter>(
|
||||||
domains.map(async (domain) => syncDomainStatus(domain))
|
domains.map(async (domain) => syncDomainStatus(domain))
|
||||||
);
|
);
|
||||||
|
|
||||||
await trySafe(async () =>
|
void trySafe(async () =>
|
||||||
Promise.all(
|
syncCustomDomainsToSamlApplicationRedirectUrls(tenantId, [...syncedDomains])
|
||||||
syncedDomains.map(async (syncedDomain) =>
|
|
||||||
syncCustomDomainToSamlApplicationRedirectUrls(tenantId, syncedDomain)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = syncedDomains.map((domain) => pick(domain, ...domainSelectFields));
|
ctx.body = syncedDomains.map((domain) => pick(domain, ...domainSelectFields));
|
||||||
|
@ -57,7 +53,13 @@ export default function domainRoutes<T extends ManagementApiRouter>(
|
||||||
const domain = await findDomainById(id);
|
const domain = await findDomainById(id);
|
||||||
const syncedDomain = await syncDomainStatus(domain);
|
const syncedDomain = await syncDomainStatus(domain);
|
||||||
|
|
||||||
await syncCustomDomainToSamlApplicationRedirectUrls(tenantId, syncedDomain);
|
void trySafe(async () => {
|
||||||
|
const domains = await findAllDomains();
|
||||||
|
const syncedDomains = await Promise.all(
|
||||||
|
domains.map(async (domain) => syncDomainStatus(domain))
|
||||||
|
);
|
||||||
|
await syncCustomDomainsToSamlApplicationRedirectUrls(tenantId, [...syncedDomains]);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = pick(syncedDomain, ...domainSelectFields);
|
ctx.body = pick(syncedDomain, ...domainSelectFields);
|
||||||
|
|
||||||
|
@ -99,7 +101,13 @@ export default function domainRoutes<T extends ManagementApiRouter>(
|
||||||
const { id } = ctx.guard.params;
|
const { id } = ctx.guard.params;
|
||||||
await deleteDomain(id);
|
await deleteDomain(id);
|
||||||
|
|
||||||
await trySafe(async () => syncCustomDomainToSamlApplicationRedirectUrls(tenantId));
|
await trySafe(async () => {
|
||||||
|
const domains = await findAllDomains();
|
||||||
|
const syncedDomains = await Promise.all(
|
||||||
|
domains.map(async (domain) => syncDomainStatus(domain))
|
||||||
|
);
|
||||||
|
await syncCustomDomainsToSamlApplicationRedirectUrls(tenantId, [...syncedDomains]);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.status = 204;
|
ctx.status = 204;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue