From 504f5b2a9952a6d99d7232a429cd0d61f9b8bf2c Mon Sep 17 00:00:00 2001 From: wangsijie Date: Wed, 3 Jul 2024 16:32:57 +0800 Subject: [PATCH] feat(core): handle oidc scopes for token exchange (#6147) * feat(core,schemas): token exchange grant * feat(core): third-party applications are not allowed for token exchange * feat(core,schemas): token exchange grant * feat(core): organization token for token exchange flow * feat(core): handle oidc scopes for token exchange --- .../core/src/oidc/grants/token-exchange.ts | 11 +++++++-- .../src/tests/api/oidc/token-exchange.test.ts | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/core/src/oidc/grants/token-exchange.ts b/packages/core/src/oidc/grants/token-exchange.ts index 03b5dbf7b..0b263995d 100644 --- a/packages/core/src/oidc/grants/token-exchange.ts +++ b/packages/core/src/oidc/grants/token-exchange.ts @@ -73,6 +73,7 @@ export const buildHandler: ( const providerInstance = instance(provider); const { features: { userinfo, resourceIndicators }, + scopes: oidcScopes, } = providerInstance.configuration(); const subjectToken = await trySafe(async () => findSubjectToken(String(params.subject_token))); @@ -186,9 +187,15 @@ export const buildHandler: ( .filter(Set.prototype.has.bind(accessToken.resourceServer.scopes)) .join(' '); } else { - // TODO: (LOG-9166) Check claims and scopes accessToken.claims = ctx.oidc.claims; - accessToken.scope = Array.from(scope).join(' '); + // Filter scopes from `oidcScopes`, + // in other grants, this is done by `Grant` class + // See https://github.com/panva/node-oidc-provider/blob/0c569cf5c36dd5faa105fb931a43b2e587530def/lib/helpers/oidc_context.js#L159 + accessToken.scope = Array.from(scope) + // Wrong typing for oidc-provider, `oidcScopes` is actully a Set, + // wrap it with `new Set` to make it work + .filter((name) => new Set(oidcScopes).has(name)) + .join(' '); } /* eslint-enable @silverhand/fp/no-mutation, @typescript-eslint/no-unsafe-assignment */ diff --git a/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts b/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts index 9dda3e0bd..b19d1e303 100644 --- a/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts +++ b/packages/integration-tests/src/tests/api/oidc/token-exchange.test.ts @@ -1,4 +1,4 @@ -import { buildOrganizationUrn } from '@logto/core-kit'; +import { UserScope, buildOrganizationUrn } from '@logto/core-kit'; import { decodeAccessToken } from '@logto/js'; import { ApplicationType, GrantType, MfaFactor } from '@logto/schemas'; import { formUrlEncodedHeaders } from '@logto/shared'; @@ -137,6 +137,28 @@ describe('Token Exchange', () => { await deleteApplication(thirdPartyApplication.id); }); + + it('should filter out non-oidc scopes', async () => { + const { subjectToken } = await createSubjectToken(userId); + + const body = await oidcApi + .post('token', { + headers: formUrlEncodedHeaders, + body: new URLSearchParams({ + client_id: applicationId, + grant_type: GrantType.TokenExchange, + subject_token: subjectToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + scope: [UserScope.Profile, 'non-oidc-scope'].join(' '), + }), + }) + .json(); + + expect(body).toHaveProperty('access_token'); + expect(body).toHaveProperty('token_type', 'Bearer'); + expect(body).toHaveProperty('expires_in'); + expect(body).toHaveProperty('scope', UserScope.Profile); + }); }); describe('get access token for organization', () => {