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:
parent
ee84462e14
commit
c3c2bf20f1
6 changed files with 44 additions and 46 deletions
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue