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

feat(core): validate custom client metadata when post or patch application (#529)

This commit is contained in:
IceHe.xyz 2022-04-13 15:23:04 +08:00 committed by GitHub
parent ee84462e14
commit c3c2bf20f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 44 additions and 46 deletions

View file

@ -24,7 +24,11 @@ export const mockApplication: Application = {
redirectUris: [],
postLogoutRedirectUris: [],
},
customClientMetadata: {},
customClientMetadata: {
corsAllowedOrigins: ['http://localhost:3000', 'http://localhost:3001', 'https://logto.dev'],
idTokenTtl: 5000,
refreshTokenTtl: 6_000_000,
},
createdAt: 1_645_334_775_356,
};

View file

@ -62,7 +62,7 @@ describe('validateMetadata', () => {
});
});
describe('corsAllowOrigin', () => {
describe('isOriginAllowed', () => {
it('should return false if there is no corsAllowOrigins', () => {
expect(isOriginAllowed('https://logto.dev', {})).toBeFalsy();
});

View file

@ -2,7 +2,6 @@ import {
ApplicationType,
CustomClientMetadata,
customClientMetadataGuard,
CustomClientMetadataKey,
OidcClientMetadata,
} from '@logto/schemas';
import { errors } from 'oidc-provider';
@ -16,31 +15,12 @@ export const buildOidcClientMetadata = (metadata?: OidcClientMetadata): OidcClie
...metadata,
});
const isOrigin = (value: string) => {
try {
const { origin } = new URL(value);
// Origin: <scheme> "://" <hostname> [ ":" <port> ]
return value === origin;
} catch {
return false;
}
};
export const validateCustomClientMetadata = (key: string, value: unknown) => {
const result = customClientMetadataGuard.pick({ [key]: true }).safeParse({ [key]: value });
if (!result.success) {
throw new errors.InvalidClientMetadata(key);
}
if (
key === CustomClientMetadataKey.CorsAllowedOrigins &&
Array.isArray(value) &&
value.some((origin) => !isOrigin(origin))
) {
throw new errors.InvalidClientMetadata(CustomClientMetadataKey.CorsAllowedOrigins);
}
};
export const isOriginAllowed = (origin: string, customClientMetadata: CustomClientMetadata) =>

View file

@ -34,6 +34,12 @@ jest.mock('@/utils/id', () => ({
buildIdGenerator: jest.fn(() => () => 'randomId'),
}));
const customClientMetadata = {
corsAllowedOrigins: ['http://localhost:5000', 'http://localhost:5001', 'https://silverhand.com'],
idTokenTtl: 999_999,
refreshTokenTtl: 100_000_000,
};
describe('application route', () => {
const applicationRequest = createRequester({ authedRoutes: applicationRoutes });
@ -47,13 +53,10 @@ describe('application route', () => {
it('POST /applications', async () => {
const name = 'FooApplication';
const type = ApplicationType.Traditional;
const description = 'New created application';
const response = await applicationRequest.post('/applications').send({
name,
type,
description,
});
const response = await applicationRequest
.post('/applications')
.send({ name, type, customClientMetadata });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
@ -61,24 +64,33 @@ describe('application route', () => {
id: 'randomId',
name,
type,
description,
customClientMetadata,
});
});
it('POST /applications should throw with invalid input body', async () => {
const name = 'FooApplication';
const type = ApplicationType.Traditional;
const description = 'New created application';
await expect(applicationRequest.post('/applications')).resolves.toHaveProperty('status', 400);
await expect(
applicationRequest.post('/applications').send({ description })
applicationRequest.post('/applications').send({ customClientMetadata })
).resolves.toHaveProperty('status', 400);
await expect(
applicationRequest.post('/applications').send({ name, description })
applicationRequest.post('/applications').send({ name, customClientMetadata })
).resolves.toHaveProperty('status', 400);
await expect(
applicationRequest.post('/applications').send({ type, description })
applicationRequest.post('/applications').send({ type, customClientMetadata })
).resolves.toHaveProperty('status', 400);
await expect(
applicationRequest.post('/applications').send({
name,
type,
customClientMetadata: {
...customClientMetadata,
corsAllowedOrigins: [''],
},
})
).resolves.toHaveProperty('status', 400);
});
@ -91,25 +103,27 @@ describe('application route', () => {
it('PATCH /applications/:applicationId', async () => {
const name = 'FooApplication';
const description = 'New created application';
const response = await applicationRequest.patch('/applications/foo').send({
name,
description,
});
const response = await applicationRequest
.patch('/applications/foo')
.send({ name, customClientMetadata });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockApplication,
name,
description,
});
expect(response.body).toEqual({ ...mockApplication, name, customClientMetadata });
});
it('PATCH /applications/:applicationId expect to throw with invalid properties', async () => {
await expect(
applicationRequest.patch('/applications/doo').send({ type: 'node' })
).resolves.toHaveProperty('status', 400);
await expect(
applicationRequest.patch('/applications/doo').send({
customClientMetadata: {
...customClientMetadata,
corsAllowedOrigins: [''],
},
})
).resolves.toHaveProperty('status', 400);
});
it('DELETE /applications/:applicationId', async () => {

View file

@ -43,14 +43,14 @@ export default function applicationRoutes<T extends AuthedRouter>(router: T) {
.merge(Applications.createGuard.pick({ name: true, type: true })),
}),
async (ctx, next) => {
const { name, type, oidcClientMetadata, ...rest } = ctx.guard.body;
const { name, type, oidcClientMetadata, customClientMetadata } = ctx.guard.body;
ctx.body = await insertApplication({
id: applicationId(),
type,
name,
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
...rest,
customClientMetadata,
});
return next();

View file

@ -35,7 +35,7 @@ export enum CustomClientMetadataKey {
}
export const customClientMetadataGuard = z.object({
[CustomClientMetadataKey.CorsAllowedOrigins]: z.string().array().optional(),
[CustomClientMetadataKey.CorsAllowedOrigins]: z.string().url().array().optional(),
[CustomClientMetadataKey.IdTokenTtl]: z.number().optional(),
[CustomClientMetadataKey.RefreshTokenTtl]: z.number().optional(),
});