diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 771d49e1a..5404e4eb1 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -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, }; diff --git a/packages/core/src/oidc/utils.test.ts b/packages/core/src/oidc/utils.test.ts index a4ccea489..ac9a50024 100644 --- a/packages/core/src/oidc/utils.test.ts +++ b/packages/core/src/oidc/utils.test.ts @@ -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(); }); diff --git a/packages/core/src/oidc/utils.ts b/packages/core/src/oidc/utils.ts index e2310a11b..c0e458806 100644 --- a/packages/core/src/oidc/utils.ts +++ b/packages/core/src/oidc/utils.ts @@ -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: "://" [ ":" ] - 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) => diff --git a/packages/core/src/routes/application.test.ts b/packages/core/src/routes/application.test.ts index 1a6930637..7aa3b2d84 100644 --- a/packages/core/src/routes/application.test.ts +++ b/packages/core/src/routes/application.test.ts @@ -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 () => { diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index 15053e352..94be881b9 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -43,14 +43,14 @@ export default function applicationRoutes(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(); diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 6631706fb..b475c7877 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -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(), });