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:
parent
bb6d88d7c4
commit
49ca7d01a8
5 changed files with 167 additions and 5 deletions
|
@ -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 */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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('-', '_')}`;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue