mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor: generate application secret on creation
This commit is contained in:
parent
e8a55b38d0
commit
ef325b25e4
7 changed files with 79 additions and 69 deletions
|
@ -1,10 +1,10 @@
|
||||||
import {
|
import {
|
||||||
type ApplicationSecret,
|
type ApplicationSecret,
|
||||||
ApplicationType,
|
|
||||||
DomainStatus,
|
DomainStatus,
|
||||||
type Application,
|
type Application,
|
||||||
type SnakeCaseOidcConfig,
|
type SnakeCaseOidcConfig,
|
||||||
internalPrefix,
|
internalPrefix,
|
||||||
|
hasSecrets,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { appendPath } from '@silverhand/essentials';
|
import { appendPath } from '@silverhand/essentials';
|
||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
@ -57,11 +57,7 @@ function EndpointsAndCredentials({
|
||||||
const [showCreateSecretModal, setShowCreateSecretModal] = useState(false);
|
const [showCreateSecretModal, setShowCreateSecretModal] = useState(false);
|
||||||
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`api/applications/${id}/secrets`);
|
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`api/applications/${id}/secrets`);
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const shouldShowAppSecrets = [
|
const shouldShowAppSecrets = hasSecrets(type);
|
||||||
ApplicationType.Traditional,
|
|
||||||
ApplicationType.MachineToMachine,
|
|
||||||
ApplicationType.Protected,
|
|
||||||
].includes(type);
|
|
||||||
|
|
||||||
const toggleShowMoreEndpoints = useCallback(() => {
|
const toggleShowMoreEndpoints = useCallback(() => {
|
||||||
setShowMoreEndpoints((previous) => !previous);
|
setShowMoreEndpoints((previous) => !previous);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
LogtoOidcConfigKey,
|
LogtoOidcConfigKey,
|
||||||
DomainStatus,
|
DomainStatus,
|
||||||
LogtoJwtTokenKey,
|
LogtoJwtTokenKey,
|
||||||
|
internalPrefix,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
|
||||||
import { protectedAppSignInCallbackUrl } from '#src/constants/index.js';
|
import { protectedAppSignInCallbackUrl } from '#src/constants/index.js';
|
||||||
|
@ -33,7 +34,7 @@ export * from './protected-app.js';
|
||||||
export const mockApplication: Application = {
|
export const mockApplication: Application = {
|
||||||
tenantId: 'fake_tenant',
|
tenantId: 'fake_tenant',
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
secret: mockId,
|
secret: internalPrefix + mockId,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
type: ApplicationType.SPA,
|
type: ApplicationType.SPA,
|
||||||
description: null,
|
description: null,
|
||||||
|
|
|
@ -53,6 +53,7 @@ const tenantContext = new MockTenant(
|
||||||
),
|
),
|
||||||
updateApplicationById,
|
updateApplicationById,
|
||||||
},
|
},
|
||||||
|
applicationSecrets: { insert: jest.fn() },
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
InternalRole,
|
InternalRole,
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
Applications,
|
Applications,
|
||||||
|
hasSecrets,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { generateStandardId, generateStandardSecret } from '@logto/shared';
|
import { generateStandardId, generateStandardSecret } from '@logto/shared';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
|
@ -40,28 +41,12 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
...[
|
...[
|
||||||
router,
|
router,
|
||||||
{
|
{
|
||||||
queries: { applications, applicationsRoles, roles },
|
queries,
|
||||||
id: tenantId,
|
id: tenantId,
|
||||||
libraries: { quota, protectedApps },
|
libraries: { quota, protectedApps },
|
||||||
},
|
},
|
||||||
]: RouterInitArgs<T>
|
]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
|
||||||
deleteApplicationById,
|
|
||||||
findApplicationById,
|
|
||||||
insertApplication,
|
|
||||||
updateApplicationById,
|
|
||||||
countApplications,
|
|
||||||
findApplications,
|
|
||||||
} = applications;
|
|
||||||
|
|
||||||
const {
|
|
||||||
findApplicationsRolesByApplicationId,
|
|
||||||
insertApplicationsRoles,
|
|
||||||
deleteApplicationRole,
|
|
||||||
findApplicationsRolesByRoleId,
|
|
||||||
} = applicationsRoles;
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/applications',
|
'/applications',
|
||||||
koaPagination({ isOptional: true }),
|
koaPagination({ isOptional: true }),
|
||||||
|
@ -107,7 +92,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
const search = parseSearchParamsForSearch(searchParams);
|
const search = parseSearchParamsForSearch(searchParams);
|
||||||
|
|
||||||
const excludeApplicationsRoles = excludeRoleId
|
const excludeApplicationsRoles = excludeRoleId
|
||||||
? await findApplicationsRolesByRoleId(excludeRoleId)
|
? await queries.applicationsRoles.findApplicationsRolesByRoleId(excludeRoleId)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const excludeApplicationIds = excludeApplicationsRoles.map(
|
const excludeApplicationIds = excludeApplicationsRoles.map(
|
||||||
|
@ -115,7 +100,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (paginationDisabled) {
|
if (paginationDisabled) {
|
||||||
ctx.body = await findApplications({
|
ctx.body = await queries.applications.findApplications({
|
||||||
search,
|
search,
|
||||||
excludeApplicationIds,
|
excludeApplicationIds,
|
||||||
excludeOrganizationId,
|
excludeOrganizationId,
|
||||||
|
@ -127,14 +112,14 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ count }, applications] = await Promise.all([
|
const [{ count }, applications] = await Promise.all([
|
||||||
countApplications({
|
queries.applications.countApplications({
|
||||||
search,
|
search,
|
||||||
excludeApplicationIds,
|
excludeApplicationIds,
|
||||||
excludeOrganizationId,
|
excludeOrganizationId,
|
||||||
isThirdParty,
|
isThirdParty,
|
||||||
types,
|
types,
|
||||||
}),
|
}),
|
||||||
findApplications(
|
queries.applications.findApplications(
|
||||||
{
|
{
|
||||||
search,
|
search,
|
||||||
excludeApplicationIds,
|
excludeApplicationIds,
|
||||||
|
@ -164,24 +149,17 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
||||||
|
|
||||||
// When creating a m2m app, should check both m2m limit and application limit.
|
await Promise.all([
|
||||||
if (rest.type === ApplicationType.MachineToMachine) {
|
rest.type === ApplicationType.MachineToMachine && quota.guardKey('machineToMachineLimit'),
|
||||||
await quota.guardKey('machineToMachineLimit');
|
rest.isThirdParty && quota.guardKey('thirdPartyApplicationsLimit'),
|
||||||
}
|
quota.guardKey('applicationsLimit'),
|
||||||
|
]);
|
||||||
// Guard third party application limit
|
|
||||||
if (rest.isThirdParty) {
|
|
||||||
await quota.guardKey('thirdPartyApplicationsLimit');
|
|
||||||
}
|
|
||||||
|
|
||||||
await quota.guardKey('applicationsLimit');
|
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
rest.type !== ApplicationType.Protected || protectedAppMetadata,
|
rest.type !== ApplicationType.Protected || protectedAppMetadata,
|
||||||
'application.protected_app_metadata_is_required'
|
'application.protected_app_metadata_is_required'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Third party applications must be traditional type
|
|
||||||
if (rest.isThirdParty) {
|
if (rest.isThirdParty) {
|
||||||
assertThat(
|
assertThat(
|
||||||
rest.type === ApplicationType.Traditional,
|
rest.type === ApplicationType.Traditional,
|
||||||
|
@ -189,11 +167,11 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const application = await insertApplication({
|
const getSecret = () =>
|
||||||
|
EnvSet.values.isDevFeaturesEnabled ? generateInternalSecret() : generateStandardSecret();
|
||||||
|
const application = await queries.applications.insertApplication({
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
secret: EnvSet.values.isDevFeaturesEnabled
|
secret: getSecret(),
|
||||||
? generateStandardSecret()
|
|
||||||
: generateInternalSecret(),
|
|
||||||
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
|
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
|
||||||
...conditional(
|
...conditional(
|
||||||
rest.type === ApplicationType.Protected &&
|
rest.type === ApplicationType.Protected &&
|
||||||
|
@ -203,18 +181,25 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (EnvSet.values.isDevFeaturesEnabled && hasSecrets(application.type)) {
|
||||||
|
await queries.applicationSecrets.insert({
|
||||||
|
name: 'Default secret',
|
||||||
|
applicationId: application.id,
|
||||||
|
value: generateStandardSecret(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (application.type === ApplicationType.Protected) {
|
if (application.type === ApplicationType.Protected) {
|
||||||
try {
|
try {
|
||||||
await protectedApps.syncAppConfigsToRemote(application.id);
|
await protectedApps.syncAppConfigsToRemote(application.id);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Delete the application if failed to sync to remote
|
// Delete the application if failed to sync to remote
|
||||||
await deleteApplicationById(application.id);
|
await queries.applications.deleteApplicationById(application.id);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = application;
|
ctx.body = application;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -238,8 +223,9 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const application = await findApplicationById(id);
|
const application = await queries.applications.findApplicationById(id);
|
||||||
const applicationsRoles = await findApplicationsRolesByApplicationId(id);
|
const applicationsRoles =
|
||||||
|
await queries.applicationsRoles.findApplicationsRolesByApplicationId(id);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...application,
|
...application,
|
||||||
|
@ -276,8 +262,8 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
// This role is NOT intended for user assignment.
|
// This role is NOT intended for user assignment.
|
||||||
if (isAdmin !== undefined) {
|
if (isAdmin !== undefined) {
|
||||||
const [applicationsRoles, internalAdminRole] = await Promise.all([
|
const [applicationsRoles, internalAdminRole] = await Promise.all([
|
||||||
findApplicationsRolesByApplicationId(id),
|
queries.applicationsRoles.findApplicationsRolesByApplicationId(id),
|
||||||
roles.findRoleByRoleName(InternalRole.Admin),
|
queries.roles.findRoleByRoleName(InternalRole.Admin),
|
||||||
]);
|
]);
|
||||||
const usedToBeAdmin = includesInternalAdminRole(applicationsRoles);
|
const usedToBeAdmin = includesInternalAdminRole(applicationsRoles);
|
||||||
|
|
||||||
|
@ -291,17 +277,17 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAdmin && !usedToBeAdmin) {
|
if (isAdmin && !usedToBeAdmin) {
|
||||||
await insertApplicationsRoles([
|
await queries.applicationsRoles.insertApplicationsRoles([
|
||||||
{ id: generateStandardId(), applicationId: id, roleId: internalAdminRole.id },
|
{ id: generateStandardId(), applicationId: id, roleId: internalAdminRole.id },
|
||||||
]);
|
]);
|
||||||
} else if (!isAdmin && usedToBeAdmin) {
|
} else if (!isAdmin && usedToBeAdmin) {
|
||||||
await deleteApplicationRole(id, internalAdminRole.id);
|
await queries.applicationsRoles.deleteApplicationRole(id, internalAdminRole.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectedAppMetadata) {
|
if (protectedAppMetadata) {
|
||||||
const { type, protectedAppMetadata: originProtectedAppMetadata } =
|
const { type, protectedAppMetadata: originProtectedAppMetadata } =
|
||||||
await findApplicationById(id);
|
await queries.applications.findApplicationById(id);
|
||||||
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
|
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
|
||||||
assertThat(
|
assertThat(
|
||||||
originProtectedAppMetadata,
|
originProtectedAppMetadata,
|
||||||
|
@ -310,7 +296,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
status: 422,
|
status: 422,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await updateApplicationById(id, {
|
await queries.applications.updateApplicationById(id, {
|
||||||
protectedAppMetadata: {
|
protectedAppMetadata: {
|
||||||
...originProtectedAppMetadata,
|
...originProtectedAppMetadata,
|
||||||
...protectedAppMetadata,
|
...protectedAppMetadata,
|
||||||
|
@ -320,7 +306,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
await protectedApps.syncAppConfigsToRemote(id);
|
await protectedApps.syncAppConfigsToRemote(id);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Revert changes on sync failure
|
// Revert changes on sync failure
|
||||||
await updateApplicationById(id, {
|
await queries.applications.updateApplicationById(id, {
|
||||||
protectedAppMetadata: originProtectedAppMetadata,
|
protectedAppMetadata: originProtectedAppMetadata,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -328,8 +314,8 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = await (Object.keys(rest).length > 0
|
ctx.body = await (Object.keys(rest).length > 0
|
||||||
? updateApplicationById(id, rest)
|
? queries.applications.updateApplicationById(id, rest)
|
||||||
: findApplicationById(id));
|
: queries.applications.findApplicationById(id));
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -344,7 +330,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { id } = ctx.guard.params;
|
const { id } = ctx.guard.params;
|
||||||
const { type, protectedAppMetadata } = await findApplicationById(id);
|
const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id);
|
||||||
if (type === ApplicationType.Protected && protectedAppMetadata) {
|
if (type === ApplicationType.Protected && protectedAppMetadata) {
|
||||||
assertThat(
|
assertThat(
|
||||||
!protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0,
|
!protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0,
|
||||||
|
@ -354,7 +340,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
await protectedApps.deleteRemoteAppConfigs(protectedAppMetadata.host);
|
await protectedApps.deleteRemoteAppConfigs(protectedAppMetadata.host);
|
||||||
}
|
}
|
||||||
// Note: will need delete cascade when application is joint with other tables
|
// Note: will need delete cascade when application is joint with other tables
|
||||||
await deleteApplicationById(id);
|
await queries.applications.deleteApplicationById(id);
|
||||||
ctx.status = 204;
|
ctx.status = 204;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApplicationType, type Application } from '@logto/schemas';
|
import { ApplicationType, hasSecrets, internalPrefix, type Application } from '@logto/schemas';
|
||||||
import { cond, noop } from '@silverhand/essentials';
|
import { cond, noop } from '@silverhand/essentials';
|
||||||
import { HTTPError } from 'ky';
|
import { HTTPError } from 'ky';
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import {
|
||||||
} from '#src/api/application.js';
|
} from '#src/api/application.js';
|
||||||
import { devFeatureTest, randomString } from '#src/utils.js';
|
import { devFeatureTest, randomString } from '#src/utils.js';
|
||||||
|
|
||||||
|
const defaultSecretName = 'Default secret';
|
||||||
|
|
||||||
devFeatureTest.describe('application secrets', () => {
|
devFeatureTest.describe('application secrets', () => {
|
||||||
const applications: Application[] = [];
|
const applications: Application[] = [];
|
||||||
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
|
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
|
||||||
|
@ -39,19 +41,29 @@ devFeatureTest.describe('application secrets', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
expect(application.secret).toMatch(new RegExp(`^${internalPrefix}`));
|
||||||
|
|
||||||
|
// Check the default secret
|
||||||
|
const secrets = await getApplicationSecrets(application.id);
|
||||||
|
if (hasSecrets(type)) {
|
||||||
|
expect(secrets).toHaveLength(1);
|
||||||
|
expect(secrets[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
applicationId: application.id,
|
||||||
|
name: defaultSecretName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(secrets).toHaveLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
const secretName = randomString();
|
const secretName = randomString();
|
||||||
const secretPromise = createApplicationSecret({
|
const secretPromise = createApplicationSecret({
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
name: secretName,
|
name: secretName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (hasSecrets(type)) {
|
||||||
[
|
|
||||||
ApplicationType.MachineToMachine,
|
|
||||||
ApplicationType.Protected,
|
|
||||||
ApplicationType.Traditional,
|
|
||||||
].includes(type)
|
|
||||||
) {
|
|
||||||
expect(await secretPromise).toEqual(
|
expect(await secretPromise).toEqual(
|
||||||
expect.objectContaining({ applicationId: application.id, name: secretName })
|
expect.objectContaining({ applicationId: application.id, name: secretName })
|
||||||
);
|
);
|
||||||
|
@ -137,8 +149,12 @@ devFeatureTest.describe('application secrets', () => {
|
||||||
expect(await getApplicationSecrets(application.id)).toEqual(
|
expect(await getApplicationSecrets(application.id)).toEqual(
|
||||||
expect.arrayContaining([secret1, secret2])
|
expect.arrayContaining([secret1, secret2])
|
||||||
);
|
);
|
||||||
await deleteApplicationSecret(application.id, secretName1);
|
|
||||||
await deleteApplicationSecret(application.id, secretName2);
|
await Promise.all([
|
||||||
|
deleteApplicationSecret(application.id, secretName1),
|
||||||
|
deleteApplicationSecret(application.id, secretName2),
|
||||||
|
deleteApplicationSecret(application.id, defaultSecretName),
|
||||||
|
]);
|
||||||
expect(await getApplicationSecrets(application.id)).toEqual([]);
|
expect(await getApplicationSecrets(application.id)).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
9
packages/schemas/src/utils/application.ts
Normal file
9
packages/schemas/src/utils/application.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ApplicationType } from '../db-entries/custom-types.js';
|
||||||
|
|
||||||
|
/** If the application type has (or can have) secrets. */
|
||||||
|
export const hasSecrets = (type: ApplicationType) =>
|
||||||
|
[
|
||||||
|
ApplicationType.MachineToMachine,
|
||||||
|
ApplicationType.Protected,
|
||||||
|
ApplicationType.Traditional,
|
||||||
|
].includes(type);
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './application.js';
|
||||||
export * from './role.js';
|
export * from './role.js';
|
||||||
export * from './management-api.js';
|
export * from './management-api.js';
|
||||||
export * from './domain.js';
|
export * from './domain.js';
|
||||||
|
|
Loading…
Add table
Reference in a new issue