Skip to content

Commit 8ee1eb9

Browse files
authored
Merge pull request #6310 from logto-io/gao-generate-secret-on-creation
refactor: generate application secret on creation
2 parents 2ba252b + ef325b2 commit 8ee1eb9

File tree

7 files changed

+79
-69
lines changed

7 files changed

+79
-69
lines changed

packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
type ApplicationSecret,
3-
ApplicationType,
43
DomainStatus,
54
type Application,
65
type SnakeCaseOidcConfig,
76
internalPrefix,
7+
hasSecrets,
88
} from '@logto/schemas';
99
import { appendPath } from '@silverhand/essentials';
1010
import { useCallback, useContext, useMemo, useState } from 'react';
@@ -57,11 +57,7 @@ function EndpointsAndCredentials({
5757
const [showCreateSecretModal, setShowCreateSecretModal] = useState(false);
5858
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`api/applications/${id}/secrets`);
5959
const api = useApi();
60-
const shouldShowAppSecrets = [
61-
ApplicationType.Traditional,
62-
ApplicationType.MachineToMachine,
63-
ApplicationType.Protected,
64-
].includes(type);
60+
const shouldShowAppSecrets = hasSecrets(type);
6561

6662
const toggleShowMoreEndpoints = useCallback(() => {
6763
setShowMoreEndpoints((previous) => !previous);

packages/core/src/__mocks__/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
LogtoOidcConfigKey,
1818
DomainStatus,
1919
LogtoJwtTokenKey,
20+
internalPrefix,
2021
} from '@logto/schemas';
2122

2223
import { protectedAppSignInCallbackUrl } from '#src/constants/index.js';
@@ -33,7 +34,7 @@ export * from './protected-app.js';
3334
export const mockApplication: Application = {
3435
tenantId: 'fake_tenant',
3536
id: 'foo',
36-
secret: mockId,
37+
secret: internalPrefix + mockId,
3738
name: 'foo',
3839
type: ApplicationType.SPA,
3940
description: null,

packages/core/src/routes/applications/application.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const tenantContext = new MockTenant(
5353
),
5454
updateApplicationById,
5555
},
56+
applicationSecrets: { insert: jest.fn() },
5657
},
5758
undefined,
5859
{

packages/core/src/routes/applications/application.ts

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
InternalRole,
66
ApplicationType,
77
Applications,
8+
hasSecrets,
89
} from '@logto/schemas';
910
import { generateStandardId, generateStandardSecret } from '@logto/shared';
1011
import { conditional } from '@silverhand/essentials';
@@ -40,28 +41,12 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
4041
...[
4142
router,
4243
{
43-
queries: { applications, applicationsRoles, roles },
44+
queries,
4445
id: tenantId,
4546
libraries: { quota, protectedApps },
4647
},
4748
]: RouterInitArgs<T>
4849
) {
49-
const {
50-
deleteApplicationById,
51-
findApplicationById,
52-
insertApplication,
53-
updateApplicationById,
54-
countApplications,
55-
findApplications,
56-
} = applications;
57-
58-
const {
59-
findApplicationsRolesByApplicationId,
60-
insertApplicationsRoles,
61-
deleteApplicationRole,
62-
findApplicationsRolesByRoleId,
63-
} = applicationsRoles;
64-
6550
router.get(
6651
'/applications',
6752
koaPagination({ isOptional: true }),
@@ -107,15 +92,15 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
10792
const search = parseSearchParamsForSearch(searchParams);
10893

10994
const excludeApplicationsRoles = excludeRoleId
110-
? await findApplicationsRolesByRoleId(excludeRoleId)
95+
? await queries.applicationsRoles.findApplicationsRolesByRoleId(excludeRoleId)
11196
: [];
11297

11398
const excludeApplicationIds = excludeApplicationsRoles.map(
11499
({ applicationId }) => applicationId
115100
);
116101

117102
if (paginationDisabled) {
118-
ctx.body = await findApplications({
103+
ctx.body = await queries.applications.findApplications({
119104
search,
120105
excludeApplicationIds,
121106
excludeOrganizationId,
@@ -127,14 +112,14 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
127112
}
128113

129114
const [{ count }, applications] = await Promise.all([
130-
countApplications({
115+
queries.applications.countApplications({
131116
search,
132117
excludeApplicationIds,
133118
excludeOrganizationId,
134119
isThirdParty,
135120
types,
136121
}),
137-
findApplications(
122+
queries.applications.findApplications(
138123
{
139124
search,
140125
excludeApplicationIds,
@@ -164,36 +149,29 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
164149
async (ctx, next) => {
165150
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
166151

167-
// When creating a m2m app, should check both m2m limit and application limit.
168-
if (rest.type === ApplicationType.MachineToMachine) {
169-
await quota.guardKey('machineToMachineLimit');
170-
}
171-
172-
// Guard third party application limit
173-
if (rest.isThirdParty) {
174-
await quota.guardKey('thirdPartyApplicationsLimit');
175-
}
176-
177-
await quota.guardKey('applicationsLimit');
152+
await Promise.all([
153+
rest.type === ApplicationType.MachineToMachine && quota.guardKey('machineToMachineLimit'),
154+
rest.isThirdParty && quota.guardKey('thirdPartyApplicationsLimit'),
155+
quota.guardKey('applicationsLimit'),
156+
]);
178157

179158
assertThat(
180159
rest.type !== ApplicationType.Protected || protectedAppMetadata,
181160
'application.protected_app_metadata_is_required'
182161
);
183162

184-
// Third party applications must be traditional type
185163
if (rest.isThirdParty) {
186164
assertThat(
187165
rest.type === ApplicationType.Traditional,
188166
'application.invalid_third_party_application_type'
189167
);
190168
}
191169

192-
const application = await insertApplication({
170+
const getSecret = () =>
171+
EnvSet.values.isDevFeaturesEnabled ? generateInternalSecret() : generateStandardSecret();
172+
const application = await queries.applications.insertApplication({
193173
id: generateStandardId(),
194-
secret: EnvSet.values.isDevFeaturesEnabled
195-
? generateStandardSecret()
196-
: generateInternalSecret(),
174+
secret: getSecret(),
197175
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
198176
...conditional(
199177
rest.type === ApplicationType.Protected &&
@@ -203,18 +181,25 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
203181
...rest,
204182
});
205183

184+
if (EnvSet.values.isDevFeaturesEnabled && hasSecrets(application.type)) {
185+
await queries.applicationSecrets.insert({
186+
name: 'Default secret',
187+
applicationId: application.id,
188+
value: generateStandardSecret(),
189+
});
190+
}
191+
206192
if (application.type === ApplicationType.Protected) {
207193
try {
208194
await protectedApps.syncAppConfigsToRemote(application.id);
209195
} catch (error: unknown) {
210196
// Delete the application if failed to sync to remote
211-
await deleteApplicationById(application.id);
197+
await queries.applications.deleteApplicationById(application.id);
212198
throw error;
213199
}
214200
}
215201

216202
ctx.body = application;
217-
218203
return next();
219204
}
220205
);
@@ -238,8 +223,9 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
238223
return next();
239224
}
240225

241-
const application = await findApplicationById(id);
242-
const applicationsRoles = await findApplicationsRolesByApplicationId(id);
226+
const application = await queries.applications.findApplicationById(id);
227+
const applicationsRoles =
228+
await queries.applicationsRoles.findApplicationsRolesByApplicationId(id);
243229

244230
ctx.body = {
245231
...application,
@@ -276,8 +262,8 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
276262
// This role is NOT intended for user assignment.
277263
if (isAdmin !== undefined) {
278264
const [applicationsRoles, internalAdminRole] = await Promise.all([
279-
findApplicationsRolesByApplicationId(id),
280-
roles.findRoleByRoleName(InternalRole.Admin),
265+
queries.applicationsRoles.findApplicationsRolesByApplicationId(id),
266+
queries.roles.findRoleByRoleName(InternalRole.Admin),
281267
]);
282268
const usedToBeAdmin = includesInternalAdminRole(applicationsRoles);
283269

@@ -291,17 +277,17 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
291277
);
292278

293279
if (isAdmin && !usedToBeAdmin) {
294-
await insertApplicationsRoles([
280+
await queries.applicationsRoles.insertApplicationsRoles([
295281
{ id: generateStandardId(), applicationId: id, roleId: internalAdminRole.id },
296282
]);
297283
} else if (!isAdmin && usedToBeAdmin) {
298-
await deleteApplicationRole(id, internalAdminRole.id);
284+
await queries.applicationsRoles.deleteApplicationRole(id, internalAdminRole.id);
299285
}
300286
}
301287

302288
if (protectedAppMetadata) {
303289
const { type, protectedAppMetadata: originProtectedAppMetadata } =
304-
await findApplicationById(id);
290+
await queries.applications.findApplicationById(id);
305291
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
306292
assertThat(
307293
originProtectedAppMetadata,
@@ -310,7 +296,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
310296
status: 422,
311297
})
312298
);
313-
await updateApplicationById(id, {
299+
await queries.applications.updateApplicationById(id, {
314300
protectedAppMetadata: {
315301
...originProtectedAppMetadata,
316302
...protectedAppMetadata,
@@ -320,16 +306,16 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
320306
await protectedApps.syncAppConfigsToRemote(id);
321307
} catch (error: unknown) {
322308
// Revert changes on sync failure
323-
await updateApplicationById(id, {
309+
await queries.applications.updateApplicationById(id, {
324310
protectedAppMetadata: originProtectedAppMetadata,
325311
});
326312
throw error;
327313
}
328314
}
329315

330316
ctx.body = await (Object.keys(rest).length > 0
331-
? updateApplicationById(id, rest)
332-
: findApplicationById(id));
317+
? queries.applications.updateApplicationById(id, rest)
318+
: queries.applications.findApplicationById(id));
333319

334320
return next();
335321
}
@@ -344,7 +330,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
344330
}),
345331
async (ctx, next) => {
346332
const { id } = ctx.guard.params;
347-
const { type, protectedAppMetadata } = await findApplicationById(id);
333+
const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id);
348334
if (type === ApplicationType.Protected && protectedAppMetadata) {
349335
assertThat(
350336
!protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0,
@@ -354,7 +340,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
354340
await protectedApps.deleteRemoteAppConfigs(protectedAppMetadata.host);
355341
}
356342
// Note: will need delete cascade when application is joint with other tables
357-
await deleteApplicationById(id);
343+
await queries.applications.deleteApplicationById(id);
358344
ctx.status = 204;
359345

360346
return next();

packages/integration-tests/src/tests/api/application/application.secrets.test.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApplicationType, type Application } from '@logto/schemas';
1+
import { ApplicationType, hasSecrets, internalPrefix, type Application } from '@logto/schemas';
22
import { cond, noop } from '@silverhand/essentials';
33
import { HTTPError } from 'ky';
44

@@ -11,6 +11,8 @@ import {
1111
} from '#src/api/application.js';
1212
import { devFeatureTest, randomString } from '#src/utils.js';
1313

14+
const defaultSecretName = 'Default secret';
15+
1416
devFeatureTest.describe('application secrets', () => {
1517
const applications: Application[] = [];
1618
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
@@ -39,19 +41,29 @@ devFeatureTest.describe('application secrets', () => {
3941
}
4042
)
4143
);
44+
expect(application.secret).toMatch(new RegExp(`^${internalPrefix}`));
45+
46+
// Check the default secret
47+
const secrets = await getApplicationSecrets(application.id);
48+
if (hasSecrets(type)) {
49+
expect(secrets).toHaveLength(1);
50+
expect(secrets[0]).toEqual(
51+
expect.objectContaining({
52+
applicationId: application.id,
53+
name: defaultSecretName,
54+
})
55+
);
56+
} else {
57+
expect(secrets).toHaveLength(0);
58+
}
59+
4260
const secretName = randomString();
4361
const secretPromise = createApplicationSecret({
4462
applicationId: application.id,
4563
name: secretName,
4664
});
4765

48-
if (
49-
[
50-
ApplicationType.MachineToMachine,
51-
ApplicationType.Protected,
52-
ApplicationType.Traditional,
53-
].includes(type)
54-
) {
66+
if (hasSecrets(type)) {
5567
expect(await secretPromise).toEqual(
5668
expect.objectContaining({ applicationId: application.id, name: secretName })
5769
);
@@ -137,8 +149,12 @@ devFeatureTest.describe('application secrets', () => {
137149
expect(await getApplicationSecrets(application.id)).toEqual(
138150
expect.arrayContaining([secret1, secret2])
139151
);
140-
await deleteApplicationSecret(application.id, secretName1);
141-
await deleteApplicationSecret(application.id, secretName2);
152+
153+
await Promise.all([
154+
deleteApplicationSecret(application.id, secretName1),
155+
deleteApplicationSecret(application.id, secretName2),
156+
deleteApplicationSecret(application.id, defaultSecretName),
157+
]);
142158
expect(await getApplicationSecrets(application.id)).toEqual([]);
143159
});
144160
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ApplicationType } from '../db-entries/custom-types.js';
2+
3+
/** If the application type has (or can have) secrets. */
4+
export const hasSecrets = (type: ApplicationType) =>
5+
[
6+
ApplicationType.MachineToMachine,
7+
ApplicationType.Protected,
8+
ApplicationType.Traditional,
9+
].includes(type);

packages/schemas/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './application.js';
12
export * from './role.js';
23
export * from './management-api.js';
34
export * from './domain.js';

0 commit comments

Comments
 (0)