From 06a1bd139462291c5a0cb5de70a4c486b17b0ace Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 15 Nov 2024 10:53:08 +0800 Subject: [PATCH] fix(core): allow all origins for profile and verification api (#6799) --- packages/core/src/middleware/koa-cors.test.ts | 112 ++++++++++-------- packages/core/src/middleware/koa-cors.ts | 13 +- packages/core/src/routes-me/init.ts | 2 +- packages/core/src/routes/init.ts | 5 +- .../src/tests/api/profile/index.test.ts | 10 ++ 5 files changed, 86 insertions(+), 56 deletions(-) diff --git a/packages/core/src/middleware/koa-cors.test.ts b/packages/core/src/middleware/koa-cors.test.ts index abdc4b233..7f4b0eae1 100644 --- a/packages/core/src/middleware/koa-cors.test.ts +++ b/packages/core/src/middleware/koa-cors.test.ts @@ -52,60 +52,72 @@ describe('koaCors() middleware', () => { process.env = { ...envBackup }; }); - it('should set proper CORS response headers for a single URL Set', async () => { - const endpoint = 'https://logto.io'; - process.env.ENDPOINT = endpoint; - process.env.NODE_ENV = 'dev'; - const urlSet = new UrlSet(false, 3001); - const run = koaCors(urlSet); + describe('with URL sets', () => { + it('should set proper CORS response headers for a single URL Set', async () => { + const endpoint = 'https://logto.io'; + process.env.ENDPOINT = endpoint; + process.env.NODE_ENV = 'dev'; + const urlSet = new UrlSet(false, 3001); + const run = koaCors([urlSet]); - const [ctx1, setSpy1] = mockContext('GET', endpoint + '/api'); - await run(ctx1, noop); - expectCorsHeaders(setSpy1, endpoint); + const [ctx1, setSpy1] = mockContext('GET', endpoint + '/api'); + await run(ctx1, noop); + expectCorsHeaders(setSpy1, endpoint); - const [ctx2, setSpy2] = mockContext('GET', 'http://localhost:3001/api'); - await run(ctx2, noop); - expectCorsHeaders(setSpy2, 'http://localhost:3001'); + const [ctx2, setSpy2] = mockContext('GET', 'http://localhost:3001/api'); + await run(ctx2, noop); + expectCorsHeaders(setSpy2, 'http://localhost:3001'); + }); + + it('should set proper CORS response headers for multiple URL Sets', async () => { + const endpoint = 'https://logto.io'; + const adminEndpoint = 'https://logto.admin'; + + process.env.ENDPOINT = endpoint; + process.env.ADMIN_ENDPOINT = adminEndpoint; + process.env.NODE_ENV = 'dev'; + const run = koaCors([new UrlSet(false, 3001), new UrlSet(true, 3002, 'ADMIN_')]); + + const [ctx1, setSpy1] = mockContext('PUT', 'https://localhost:3002/api'); + await run(ctx1, noop); + expectCorsHeaders(setSpy1, 'https://localhost:3002'); + + const [ctx2, setSpy2] = mockContext('POST', adminEndpoint + '/api'); + await run(ctx2, noop); + expectCorsHeaders(setSpy2, adminEndpoint); + }); + + it('should set CORS response headers for localhost in production when endpoint is unavailable', async () => { + process.env.ENDPOINT = undefined; + process.env.NODE_ENV = 'production'; + const urlSet = new UrlSet(true, 3002); + const run = koaCors([urlSet]); + + const [ctx, setSpy] = mockContext('POST', 'https://localhost:3002/api'); + await run(ctx, noop); + expectCorsHeaders(setSpy, 'https://localhost:3002'); + }); + + it('should not to set CORS response headers for localhost in production when endpoint is available', async () => { + const endpoint = 'https://logto.io'; + process.env.ENDPOINT = endpoint; + process.env.NODE_ENV = 'production'; + const urlSet = new UrlSet(false, 3001); + const run = koaCors([urlSet]); + + const [ctx, setSpy] = mockContext('DELETE', 'http://localhost:3001/api'); + await run(ctx, noop); + expectCorsHeaders(setSpy, ''); + }); }); - it('should set proper CORS response headers for multiple URL Sets', async () => { - const endpoint = 'https://logto.io'; - const adminEndpoint = 'https://logto.admin'; + describe('with allowed prefixes', () => { + it('should allow any origin if the path starts with an allowed prefix', async () => { + const run = koaCors([], ['/api']); - process.env.ENDPOINT = endpoint; - process.env.ADMIN_ENDPOINT = adminEndpoint; - process.env.NODE_ENV = 'dev'; - const run = koaCors(new UrlSet(false, 3001), new UrlSet(true, 3002, 'ADMIN_')); - - const [ctx1, setSpy1] = mockContext('PUT', 'https://localhost:3002/api'); - await run(ctx1, noop); - expectCorsHeaders(setSpy1, 'https://localhost:3002'); - - const [ctx2, setSpy2] = mockContext('POST', adminEndpoint + '/api'); - await run(ctx2, noop); - expectCorsHeaders(setSpy2, adminEndpoint); - }); - - it('should set CORS response headers for localhost in production when endpoint is unavailable', async () => { - process.env.ENDPOINT = undefined; - process.env.NODE_ENV = 'production'; - const urlSet = new UrlSet(true, 3002); - const run = koaCors(urlSet); - - const [ctx, setSpy] = mockContext('POST', 'https://localhost:3002/api'); - await run(ctx, noop); - expectCorsHeaders(setSpy, 'https://localhost:3002'); - }); - - it('should not to set CORS response headers for localhost in production when endpoint is available', async () => { - const endpoint = 'https://logto.io'; - process.env.ENDPOINT = endpoint; - process.env.NODE_ENV = 'production'; - const urlSet = new UrlSet(false, 3001); - const run = koaCors(urlSet); - - const [ctx, setSpy] = mockContext('DELETE', 'http://localhost:3001/api'); - await run(ctx, noop); - expectCorsHeaders(setSpy, ''); + const [ctx, setSpy] = mockContext('GET', 'https://logto.io/api'); + await run(ctx, noop); + expectCorsHeaders(setSpy, 'https://logto.io'); + }); }); }); diff --git a/packages/core/src/middleware/koa-cors.ts b/packages/core/src/middleware/koa-cors.ts index 89c3c3493..a3c014fb9 100644 --- a/packages/core/src/middleware/koa-cors.ts +++ b/packages/core/src/middleware/koa-cors.ts @@ -5,11 +5,20 @@ import type { MiddlewareType } from 'koa'; import { EnvSet } from '#src/env-set/index.js'; export default function koaCors( - ...urlSets: UrlSet[] + urlSets: UrlSet[], + allowedPrefixes: string[] = [] ): MiddlewareType { return cors({ origin: (ctx) => { - const { origin } = ctx.request.headers; + const { + headers: { origin }, + path, + } = ctx.request; + + // Allow any origin if the path starts with an allowed prefix + if (allowedPrefixes.some((prefix) => path.startsWith(prefix))) { + return origin ?? '*'; + } if (!EnvSet.values.isProduction) { return origin ?? ''; diff --git a/packages/core/src/routes-me/init.ts b/packages/core/src/routes-me/init.ts index a689b6972..17ce78d04 100644 --- a/packages/core/src/routes-me/init.ts +++ b/packages/core/src/routes-me/init.ts @@ -40,7 +40,7 @@ export default function initMeApis(tenant: TenantContext): Koa { userAssetsRoutes(meRouter, tenant); const meApp = new Koa(); - meApp.use(koaCors(EnvSet.values.cloudUrlSet)); + meApp.use(koaCors([EnvSet.values.cloudUrlSet])); meApp.use(meRouter.routes()).use(meRouter.allowedMethods()); return meApp; diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 0003bb12b..2ec66ad3f 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -5,13 +5,13 @@ import Router from 'koa-router'; import { EnvSet } from '#src/env-set/index.js'; import koaAuditLog from '#src/middleware/koa-audit-log.js'; import koaBodyEtag from '#src/middleware/koa-body-etag.js'; -import koaCors from '#src/middleware/koa-cors.js'; import { koaManagementApiHooks } from '#src/middleware/koa-management-api-hooks.js'; import koaTenantGuard from '#src/middleware/koa-tenant-guard.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import koaAuth from '../middleware/koa-auth/index.js'; import koaOidcAuth from '../middleware/koa-auth/koa-oidc-auth.js'; +import koaCors from '../middleware/koa-cors.js'; import accountCentersRoutes from './account-center/index.js'; import adminUserRoutes from './admin-user/index.js'; @@ -135,9 +135,8 @@ const createRouters = (tenant: TenantContext) => { export default function initApis(tenant: TenantContext): Koa { const apisApp = new Koa(); - const { adminUrlSet, cloudUrlSet } = EnvSet.values; - apisApp.use(koaCors(adminUrlSet, cloudUrlSet)); + apisApp.use(koaCors([adminUrlSet, cloudUrlSet], ['/profile', '/verifications'])); apisApp.use(koaBodyEtag()); for (const router of createRouters(tenant)) { diff --git a/packages/integration-tests/src/tests/api/profile/index.test.ts b/packages/integration-tests/src/tests/api/profile/index.test.ts index 1ae981a9f..e160711e2 100644 --- a/packages/integration-tests/src/tests/api/profile/index.test.ts +++ b/packages/integration-tests/src/tests/api/profile/index.test.ts @@ -48,6 +48,16 @@ describe('profile', () => { }); describe('GET /profile', () => { + it('should allow all origins', async () => { + const { user, username, password } = await createDefaultTenantUserWithPassword(); + const api = await signInAndGetUserApi(username, password); + const response = await api.get('api/profile'); + expect(response.status).toBe(200); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); + + await deleteDefaultTenantUser(user.id); + }); + it('should be able to get profile with default scopes', async () => { const { user, username, password } = await createDefaultTenantUserWithPassword(); const api = await signInAndGetUserApi(username, password);