diff --git a/.changeset/rare-lamps-worry.md b/.changeset/rare-lamps-worry.md new file mode 100644 index 000000000..12d84219b --- /dev/null +++ b/.changeset/rare-lamps-worry.md @@ -0,0 +1,9 @@ +--- +"@logto/core": patch +--- + +Support comma separated resource parameter + +Some third-party libraries or plugins do not support array of resources, and can only specify `resource` through `additionalParameters` config, e.g. `flutter-appauth`. However, only one resource can be specified at a time in this way. This PR enables comma separated resource parameter support in Logto core service, so that multiple resources can be specified via a single string. + +For example: Auth URL like `/oidc/auth?resource=https://example.com/api1,https://example.com/api2` will be interpreted and parsed to Logto core service as `/ordc/auth?resource=https://example.com/api1&resource=https://example.com/api2`. diff --git a/packages/core/src/middleware/koa-resource-param.test.ts b/packages/core/src/middleware/koa-resource-param.test.ts new file mode 100644 index 000000000..235f8fa03 --- /dev/null +++ b/packages/core/src/middleware/koa-resource-param.test.ts @@ -0,0 +1,45 @@ +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; + +const { jest } = import.meta; + +const { default: koaResourceParam } = await import('./koa-resource-param.js'); + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = async () => {}; + +const endpoint = 'https://logto.io/oidc/auth'; + +describe('koaResourceParam() middleware', () => { + it('should check and process comma separated resource params in the URL', async () => { + const ctx = createMockContext({ url: endpoint + '?resource=foo,bar' }); + const run = koaResourceParam(); + + await run(ctx, noop); + + expect(ctx.request.query).toEqual({ + resource: ['foo', 'bar'], + }); + }); + + it('should also work with both comma separated and single resource params', async () => { + const ctx = createMockContext({ url: endpoint + '?resource=foo,bar&resource=baz' }); + const run = koaResourceParam(); + + await run(ctx, noop); + + expect(ctx.request.query).toEqual({ + resource: ['foo', 'bar', 'baz'], + }); + }); + + it('should not affect the URL if no comma separated resource params are found', async () => { + const ctx = createMockContext({ url: endpoint + '?resource=foo&resource=bar' }); + const run = koaResourceParam(); + + await run(ctx, noop); + + expect(ctx.request.query).toEqual({ + resource: ['foo', 'bar'], + }); + }); +}); diff --git a/packages/core/src/middleware/koa-resource-param.ts b/packages/core/src/middleware/koa-resource-param.ts new file mode 100644 index 000000000..8728407df --- /dev/null +++ b/packages/core/src/middleware/koa-resource-param.ts @@ -0,0 +1,31 @@ +import type { Nullable } from '@silverhand/essentials'; +import type { MiddlewareType } from 'koa'; + +/** + * Create a middleware function that checks if the request URL contains comma separated `resource` query parameter. + * If yes, split the values and reconstruct the URL with multiple `resource` query parameters. + * E.g. `?resource=foo,bar` => `?resource=foo&resource=bar` + */ +export default function koaResourceParam(): MiddlewareType< + StateT, + ContextT, + Nullable +> { + return async (ctx, next) => { + const { query } = ctx.request; + const { resource } = query; + + if (!resource) { + return next(); + } + + const resources = Array.isArray(resource) ? resource : [resource]; + const resourceParams = resources.flatMap((resource) => resource.split(',')); + + ctx.request.query = { + ...query, + resource: resourceParams, + }; + return next(); + }; +} diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 4d9ff4d7c..769985d15 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -28,6 +28,7 @@ import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js' import { type LogtoConfigLibrary } from '#src/libraries/logto-config.js'; import koaAuditLog from '#src/middleware/koa-audit-log.js'; import koaBodyEtag from '#src/middleware/koa-body-etag.js'; +import koaResourceParam from '#src/middleware/koa-resource-param.js'; import postgresAdapter from '#src/oidc/adapter.js'; import { buildLoginPromptUrl, @@ -377,6 +378,12 @@ export default function initOidc( throw error; } }); + /** + * Check if the request URL contains comma separated `resource` query parameter. If yes, split the values and + * reconstruct the URL with multiple `resource` query parameters. + * E.g. `?resource=foo,bar` => `?resource=foo&resource=bar` + */ + oidc.use(koaResourceParam()); /** * `oidc-provider` [strictly checks](https://github.com/panva/node-oidc-provider/blob/6a0bcbcd35ed3e6179e81f0ab97a45f5e4e58f48/lib/shared/selective_body.js#L11) * the `content-type` header for further processing.