0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): add app unknown session fallback uri validation

add app unknown session fallback uri validation
This commit is contained in:
simeng-li 2024-11-08 15:25:18 +08:00
parent bb6d88d7c4
commit 49ca7d01a8
No known key found for this signature in database
GPG key ID: 14EA7BB1541E8075
5 changed files with 167 additions and 5 deletions

View file

@ -1,6 +1,8 @@
/* eslint-disable max-lines */
// TODO: @darcyYe refactor this file later to remove disable max line comment
import type { Role } from '@logto/schemas';
import { isValidUrl } from '@logto/core-kit';
import type { CreateApplication, Role } from '@logto/schemas';
import {
Applications,
ApplicationType,
@ -10,9 +12,10 @@ import {
InternalRole,
} from '@logto/schemas';
import { generateStandardId, generateStandardSecret } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
import { conditional, type Nullable } from '@silverhand/essentials';
import { boolean, object, string, z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
@ -24,7 +27,7 @@ import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
import applicationCustomDataRoutes from './application-custom-data.js';
import { generateInternalSecret } from './application-secret.js';
import { applicationCreateGuard, applicationPatchGuard } from './types.js';
import { applicationCreateGuard, applicationPatchGuard, applicationTypeGuard } from './types.js';
const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
roles.some(({ role: { name } }) => name === InternalRole.Admin);
@ -36,8 +39,25 @@ const parseIsThirdPartQueryParam = (isThirdPartyQuery: 'true' | 'false' | undefi
return isThirdPartyQuery === 'true';
};
const validateApplicationUnknownSessionFallbackUri = ({
type,
unknownSessionFallbackUri,
isThirdParty,
}: Partial<CreateApplication>) => {
if (!unknownSessionFallbackUri || !EnvSet.values.isDevFeaturesEnabled) {
return;
}
const applicationTypeGuard = z.nativeEnum(ApplicationType);
assertThat(
isValidUrl(unknownSessionFallbackUri),
'application.invalid_unknown_session_fallback_uri'
);
assertThat(
type !== ApplicationType.MachineToMachine && !isThirdParty,
'application.unknown_session_fallback_uri_not_supported'
);
};
export default function applicationRoutes<T extends ManagementApiRouter>(
...[router, tenant]: RouterInitArgs<T>
@ -170,6 +190,8 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
);
}
validateApplicationUnknownSessionFallbackUri(rest);
const application = await queries.applications.insertApplication({
id: generateStandardId(),
secret: generateInternalSecret(),
@ -252,7 +274,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
})
),
response: Applications.guard,
status: [200, 404, 422, 500],
status: [200, 400, 404, 422, 500],
}),
async (ctx, next) => {
const {
@ -319,6 +341,22 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
}
}
// @ts-expect-error -- fix me once devFeature guard is removed
// eslint-disable-next-line no-restricted-syntax
const unknownSessionFallbackUri = rest.unknownSessionFallbackUri as Nullable<
string | undefined
>;
if (unknownSessionFallbackUri) {
const { type, isThirdParty } = await queries.applications.findApplicationById(id);
// Validate the unknownSessionFallbackUri
validateApplicationUnknownSessionFallbackUri({
type,
isThirdParty,
unknownSessionFallbackUri,
});
}
ctx.body = await (Object.keys(rest).length > 0
? queries.applications.updateApplicationById(id, rest, 'replace')
: queries.applications.findApplicationById(id));
@ -359,3 +397,4 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
applicationCustomDataRoutes(router, tenant);
}
/* eslint-enable max-lines */

View file

@ -1,12 +1,18 @@
import {
ApplicationType,
applicationCreateGuard as originalApplicationCreateGuard,
applicationPatchGuard as originalApplicationPatchGuard,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
export const applicationCreateGuard = originalApplicationCreateGuard
.omit({
protectedAppMetadata: true,
// TODO: @simeng remove this conditional when the feature is enabled
...conditional(!EnvSet.values.isDevFeaturesEnabled && { unknownSessionFallbackUri: true }),
})
.extend({
protectedAppMetadata: z
@ -20,6 +26,8 @@ export const applicationCreateGuard = originalApplicationCreateGuard
export const applicationPatchGuard = originalApplicationPatchGuard
.omit({
protectedAppMetadata: true,
// TODO: @simeng remove this conditional when the feature is enabled
...conditional(!EnvSet.values.isDevFeaturesEnabled && { unknownSessionFallbackUri: true }),
})
.extend({
protectedAppMetadata: z
@ -37,3 +45,5 @@ export const applicationPatchGuard = originalApplicationPatchGuard
})
.optional(),
});
export const applicationTypeGuard = z.nativeEnum(ApplicationType);

View file

@ -0,0 +1,108 @@
import { ApplicationType } from '@logto/schemas';
import { createApplication, deleteApplication, updateApplication } from '#src/api/index.js';
import { expectRejects } from '#src/helpers/index.js';
import { devFeatureTest, generateUuid } from '#src/utils.js';
devFeatureTest.describe('application unknown session fallback uri API tests', () => {
describe('create application', () => {
const unknownSessionFallbackUri = 'https://example.com';
it('should throw invalid_unknown_session_fallback_uri error when creating application with invalid unknown session fallback uri', async () => {
await expectRejects(
createApplication(generateUuid(), ApplicationType.SPA, {
unknownSessionFallbackUri: 'invalid-uri',
}),
{ code: 'application.invalid_unknown_session_fallback_uri', status: 400 }
);
});
it('should throw unknown_session_fallback_uri_not_supported error when creating machine-to-machine application', async () => {
await expectRejects(
createApplication(generateUuid(), ApplicationType.MachineToMachine, {
unknownSessionFallbackUri,
}),
{ code: 'application.unknown_session_fallback_uri_not_supported', status: 400 }
);
});
it('should throw unknown_session_fallback_uri_not_supported error when creating third-party application', async () => {
await expectRejects(
createApplication(generateUuid(), ApplicationType.Traditional, {
unknownSessionFallbackUri,
isThirdParty: true,
}),
{ code: 'application.unknown_session_fallback_uri_not_supported', status: 400 }
);
});
it('should create application with unknown session fallback uri successfully', async () => {
const application = await createApplication(generateUuid(), ApplicationType.SPA, {
unknownSessionFallbackUri,
});
expect(application.unknownSessionFallbackUri).toBe(unknownSessionFallbackUri);
await deleteApplication(application.id);
});
});
describe('update application', () => {
it('should throw invalid_unknown_session_fallback_uri error when updating application with invalid unknown session fallback uri', async () => {
const application = await createApplication(generateUuid(), ApplicationType.SPA);
await expectRejects(
updateApplication(application.id, { unknownSessionFallbackUri: 'invalid-uri' }),
{
code: 'application.invalid_unknown_session_fallback_uri',
status: 400,
}
);
await deleteApplication(application.id);
});
it('should throw unknown_session_fallback_uri_not_supported error when updating machine-to-machine application', async () => {
const application = await createApplication(generateUuid(), ApplicationType.MachineToMachine);
await expectRejects(
updateApplication(application.id, { unknownSessionFallbackUri: 'https://example.com' }),
{
code: 'application.unknown_session_fallback_uri_not_supported',
status: 400,
}
);
await deleteApplication(application.id);
});
it('should throw unknown_session_fallback_uri_not_supported error when updating third-party application', async () => {
const application = await createApplication(generateUuid(), ApplicationType.Traditional, {
isThirdParty: true,
});
await expectRejects(
updateApplication(application.id, { unknownSessionFallbackUri: 'https://example.com' }),
{
code: 'application.unknown_session_fallback_uri_not_supported',
status: 400,
}
);
await deleteApplication(application.id);
});
it('should update application with unknown session fallback uri successfully', async () => {
const application = await createApplication(generateUuid(), ApplicationType.SPA);
const unknownSessionFallbackUri = 'https://example.com';
const updatedApplication = await updateApplication(application.id, {
unknownSessionFallbackUri,
});
expect(updatedApplication.unknownSessionFallbackUri).toBe(unknownSessionFallbackUri);
const removedUnknownSessionFallbackUriApplication = await updateApplication(application.id, {
unknownSessionFallbackUri: null,
});
expect(removedUnknownSessionFallbackUriApplication.unknownSessionFallbackUri).toBe(null);
await deleteApplication(application.id);
});
});
});

View file

@ -7,6 +7,7 @@ import { type Page } from 'puppeteer';
import { isDevFeaturesEnabled } from './constants.js';
export const generateUuid = () => crypto.randomUUID();
export const generateName = () => crypto.randomUUID();
export const generateUserId = () => crypto.randomUUID();
export const generateUsername = () => `usr_${crypto.randomUUID().replaceAll('-', '_')}`;

View file

@ -20,6 +20,10 @@ const application = {
should_delete_custom_domains_first: 'Should delete custom domains first.',
no_legacy_secret_found: 'The application does not have a legacy secret.',
secret_name_exists: 'Secret name already exists.',
invalid_unknown_session_fallback_uri:
'The session fallback URI is invalid. It must be in a valid URI format.',
unknown_session_fallback_uri_not_supported:
'Unknown session fallback URI is not supported for this application type.',
};
export default Object.freeze(application);