From 2ae998e561e6e61d9b4929b452bcbe1ca570f763 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 5 Jul 2024 11:12:19 +0800 Subject: [PATCH] feat(console,schemas): add grant context to custom jwt --- ...generate-jwt-customizer-type-definition.ts | 9 +++++++ .../InstructionTab/GuideCard/index.tsx | 1 + .../SettingsSection/InstructionTab/index.tsx | 19 ++++++++++++++ .../CustomizeJwtDetails/utils/config.tsx | 21 +++++++++++---- .../utils/type-definitions.ts | 4 +++ .../grants/token-exchange/actor-token.test.ts | 3 ++- .../oidc/grants/token-exchange/actor-token.ts | 13 +++++----- .../oidc/grants/token-exchange/index.test.ts | 4 ++- .../src/oidc/grants/token-exchange/index.ts | 6 ++--- .../src/oidc/grants/token-exchange/types.ts | 4 +++ packages/core/src/oidc/init.ts | 26 +++++++++---------- .../translation/admin-console/jwt-claims.ts | 5 ++++ .../src/types/logto-config/jwt-customizer.ts | 15 ++++++++++- 13 files changed, 98 insertions(+), 32 deletions(-) diff --git a/packages/console/scripts/generate-jwt-customizer-type-definition.ts b/packages/console/scripts/generate-jwt-customizer-type-definition.ts index 9b25077a20b..e7f9af524d2 100644 --- a/packages/console/scripts/generate-jwt-customizer-type-definition.ts +++ b/packages/console/scripts/generate-jwt-customizer-type-definition.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import { accessTokenPayloadGuard, clientCredentialsPayloadGuard, + jwtCustomizerGrantContextGuard, jwtCustomizerUserContextGuard, } from '@logto/schemas'; import prettier from 'prettier'; @@ -13,6 +14,7 @@ const filePath = 'src/consts/jwt-customizer-type-definition.ts'; const typeIdentifiers = `export enum JwtCustomizerTypeDefinitionKey { JwtCustomizerUserContext = 'JwtCustomizerUserContext', + JwtCustomizerGrantContext = 'JwtCustomizerGrantContext', AccessTokenPayload = 'AccessTokenPayload', ClientCredentialsPayload = 'ClientCredentialsPayload', EnvironmentVariables = 'EnvironmentVariables', @@ -43,6 +45,11 @@ const createJwtCustomizerTypeDefinitions = async () => { 'JwtCustomizerUserContext' ); + const jwtCustomizerGrantContextTypeDefinition = inferTsDefinitionFromZod( + jwtCustomizerGrantContextGuard, + 'JwtCustomizerGrantContext' + ); + const accessTokenPayloadTypeDefinition = inferTsDefinitionFromZod( accessTokenPayloadGuard, 'AccessTokenPayload' @@ -58,6 +65,8 @@ ${typeIdentifiers} export const jwtCustomizerUserContextTypeDefinition = \`${jwtCustomizerUserContextTypeDefinition}\`; +export const jwtCustomizerGrantContextTypeDefinition = \`${jwtCustomizerGrantContextTypeDefinition}\`; + export const accessTokenPayloadTypeDefinition = \`${accessTokenPayloadTypeDefinition}\`; export const clientCredentialsPayloadTypeDefinition = \`${clientCredentialsPayloadTypeDefinition}\`; diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/GuideCard/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/GuideCard/index.tsx index 1a087ca5711..5472e3e776b 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/GuideCard/index.tsx +++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/GuideCard/index.tsx @@ -9,6 +9,7 @@ import * as styles from './index.module.scss'; export enum CardType { UserData = 'user_data', + GrantData = 'grant_data', TokenData = 'token_data', FetchExternalData = 'fetch_external_data', EnvironmentVariables = 'environment_variables', diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx index f34685c80af..dc7bbd68f42 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx +++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx @@ -16,6 +16,7 @@ import { accessTokenPayloadTypeDefinition, clientCredentialsPayloadTypeDefinition, jwtCustomizerUserContextTypeDefinition, + jwtCustomizerGrantContextTypeDefinition, } from '@/pages/CustomizeJwtDetails/utils/type-definitions'; import * as tabContentStyles from '../index.module.scss'; @@ -77,6 +78,24 @@ function InstructionTab({ isActive }: Props) { /> )} + {tokenType === LogtoJwtTokenKeyType.AccessToken && ( + { + setExpendCard(expand ? CardType.GrantData : undefined); + }} + > + + + )} {} */ declare type Context = { user: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}; + grant?: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerGrantContext}; } declare type Payload = { @@ -199,8 +202,16 @@ const defaultUserContext: Partial = { organizationRoles: [], }; +const defaultGrantContext: Partial = { + type: GrantType.TokenExchange, + subjectTokenContext: { + foo: 'bar', + }, +}; + export const defaultUserTokenContextData = { user: defaultUserContext, + grant: defaultGrantContext, }; export const accessTokenPayloadTestModel: ModelSettings = { @@ -223,6 +234,6 @@ export const userContextTestModel: ModelSettings = { language: 'json', icon: , name: 'user-token-context.json', - title: 'User data', + title: 'Context data', defaultValue: JSON.stringify(defaultUserTokenContextData, null, 2), }; diff --git a/packages/console/src/pages/CustomizeJwtDetails/utils/type-definitions.ts b/packages/console/src/pages/CustomizeJwtDetails/utils/type-definitions.ts index a5e3fe8f8a9..ed9240e355d 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/utils/type-definitions.ts +++ b/packages/console/src/pages/CustomizeJwtDetails/utils/type-definitions.ts @@ -3,6 +3,7 @@ import { accessTokenPayloadTypeDefinition, clientCredentialsPayloadTypeDefinition, jwtCustomizerUserContextTypeDefinition, + jwtCustomizerGrantContextTypeDefinition, } from '@/consts/jwt-customizer-type-definition'; import { type JwtCustomizerForm } from '../type'; @@ -12,11 +13,14 @@ export { accessTokenPayloadTypeDefinition, clientCredentialsPayloadTypeDefinition, jwtCustomizerUserContextTypeDefinition, + jwtCustomizerGrantContextTypeDefinition, } from '@/consts/jwt-customizer-type-definition'; export const buildAccessTokenJwtCustomizerContextTsDefinition = () => { return `declare ${jwtCustomizerUserContextTypeDefinition} + declare ${jwtCustomizerGrantContextTypeDefinition} + declare ${accessTokenPayloadTypeDefinition}`; }; diff --git a/packages/core/src/oidc/grants/token-exchange/actor-token.test.ts b/packages/core/src/oidc/grants/token-exchange/actor-token.test.ts index 6431e29f3d5..3661a88b32d 100644 --- a/packages/core/src/oidc/grants/token-exchange/actor-token.test.ts +++ b/packages/core/src/oidc/grants/token-exchange/actor-token.test.ts @@ -4,6 +4,7 @@ import Sinon from 'sinon'; import { createOidcContext } from '#src/test-utils/oidc-provider.js'; import { handleActorToken } from './actor-token.js'; +import { TokenExchangeTokenType } from './types.js'; const { InvalidGrant } = errors; @@ -12,7 +13,7 @@ const accountId = 'some_account_id'; const validOidcContext: Partial = { params: { actor_token: 'some_actor_token', - actor_token_type: 'urn:ietf:params:oauth:token-type:access_token', + actor_token_type: TokenExchangeTokenType.AccessToken, }, }; diff --git a/packages/core/src/oidc/grants/token-exchange/actor-token.ts b/packages/core/src/oidc/grants/token-exchange/actor-token.ts index 9c369199a95..37ef0491aa3 100644 --- a/packages/core/src/oidc/grants/token-exchange/actor-token.ts +++ b/packages/core/src/oidc/grants/token-exchange/actor-token.ts @@ -3,27 +3,26 @@ import { type KoaContextWithOIDC, errors } from 'oidc-provider'; import assertThat from '#src/utils/assert-that.js'; +import { TokenExchangeTokenType } from './types.js'; + const { InvalidGrant } = errors; /** * Handles the `actor_token` and `actor_token_type` parameters, * if both are present and valid, the `accountId` of the actor token is returned. */ -export const handleActorToken = async ( - ctx: KoaContextWithOIDC -): Promise<{ accountId?: string }> => { +export const handleActorToken = async (ctx: KoaContextWithOIDC): Promise<{ actorId?: string }> => { const { params, provider } = ctx.oidc; const { AccessToken } = provider; assertThat(params, new InvalidGrant('parameters must be available')); assertThat( - !params.actor_token || - params.actor_token_type === 'urn:ietf:params:oauth:token-type:access_token', + !params.actor_token || params.actor_token_type === TokenExchangeTokenType.AccessToken, new InvalidGrant('unsupported actor token type') ); if (!params.actor_token) { - return { accountId: undefined }; + return { actorId: undefined }; } // The actor token should have `openid` scope (RFC 0005), and a token with this scope is an opaque token. @@ -35,5 +34,5 @@ export const handleActorToken = async ( new InvalidGrant('actor token must have openid scope') ); - return { accountId: actorToken.accountId }; + return { actorId: actorToken.accountId }; }; diff --git a/packages/core/src/oidc/grants/token-exchange/index.test.ts b/packages/core/src/oidc/grants/token-exchange/index.test.ts index b0adcd52b26..7dbf37fcc29 100644 --- a/packages/core/src/oidc/grants/token-exchange/index.test.ts +++ b/packages/core/src/oidc/grants/token-exchange/index.test.ts @@ -7,6 +7,8 @@ import { mockApplication } from '#src/__mocks__/index.js'; import { createOidcContext } from '#src/test-utils/oidc-provider.js'; import { MockTenant } from '#src/test-utils/tenant.js'; +import { TokenExchangeTokenType } from './types.js'; + const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); @@ -63,7 +65,7 @@ const validSubjectToken: SubjectToken = { const validOidcContext: Partial = { params: { subject_token: 'some_subject_token', - subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + subject_token_type: TokenExchangeTokenType.AccessToken, }, entities: { Client: validClient, diff --git a/packages/core/src/oidc/grants/token-exchange/index.ts b/packages/core/src/oidc/grants/token-exchange/index.ts index ad8286b0aa3..f672bad78e6 100644 --- a/packages/core/src/oidc/grants/token-exchange/index.ts +++ b/packages/core/src/oidc/grants/token-exchange/index.ts @@ -24,7 +24,7 @@ import { } from '../../resource.js'; import { handleActorToken } from './actor-token.js'; -import { type TokenExchangeAct } from './types.js'; +import { TokenExchangeTokenType, type TokenExchangeAct } from './types.js'; const { InvalidClient, InvalidGrant, AccessDenied } = errors; @@ -70,7 +70,7 @@ export const buildHandler: ( new InvalidClient('third-party applications are not allowed for this grant type') ); assertThat( - params.subject_token_type === 'urn:ietf:params:oauth:token-type:access_token', + params.subject_token_type === TokenExchangeTokenType.AccessToken, new InvalidGrant('unsupported subject token type') ); @@ -203,7 +203,7 @@ export const buildHandler: ( } // Handle the actor token - const { accountId: actorId } = await handleActorToken(ctx); + const { actorId } = await handleActorToken(ctx); if (actorId) { // The JWT generator in node-oidc-provider only recognizes a fixed list of claims, // to add other claims to JWT, the only way is to return them in `extraTokenClaims` function. diff --git a/packages/core/src/oidc/grants/token-exchange/types.ts b/packages/core/src/oidc/grants/token-exchange/types.ts index 6614ecac412..019c6826969 100644 --- a/packages/core/src/oidc/grants/token-exchange/types.ts +++ b/packages/core/src/oidc/grants/token-exchange/types.ts @@ -7,3 +7,7 @@ export const tokenExchangeActGuard = z.object({ }); export type TokenExchangeAct = z.infer; + +export enum TokenExchangeTokenType { + AccessToken = 'urn:ietf:params:oauth:token-type:access_token', +} diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 99e9ffbd86e..0342ba3f520 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -225,20 +225,18 @@ export default function initOidc( }, extraParams: Object.values(ExtraParamsKey), extraTokenClaims: async (ctx, token) => { - const tokenExchangeClaims = await getExtraTokenClaimsForTokenExchange(ctx, token); - - const organizationApiResourceClaims = await getExtraTokenClaimsForOrganizationApiResource( - ctx, - token - ); - - const jwtCustomizedClaims = await getExtraTokenClaimsForJwtCustomization(ctx, token, { - envSet, - queries, - libraries, - logtoConfigs, - cloudConnection, - }); + const [tokenExchangeClaims, organizationApiResourceClaims, jwtCustomizedClaims] = + await Promise.all([ + getExtraTokenClaimsForTokenExchange(ctx, token), + getExtraTokenClaimsForOrganizationApiResource(ctx, token), + getExtraTokenClaimsForJwtCustomization(ctx, token, { + envSet, + queries, + libraries, + logtoConfigs, + cloudConnection, + }), + ]); if (!organizationApiResourceClaims && !jwtCustomizedClaims && !tokenExchangeClaims) { return; diff --git a/packages/phrases/src/locales/en/translation/admin-console/jwt-claims.ts b/packages/phrases/src/locales/en/translation/admin-console/jwt-claims.ts index 645c4c14eb1..fe56cd08d23 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/jwt-claims.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/jwt-claims.ts @@ -30,6 +30,11 @@ const jwt_claims = { title: 'User data', subtitle: 'Use `data.user` input parameter to provide vital user info.', }, + grant_data: { + title: 'Grant data', + subtitle: + 'Use `data.grant` input parameter to provide vital grant info, only available for token exchange.', + }, token_data: { title: 'Token data', subtitle: 'Use `token` input parameter for current access token payload. ', diff --git a/packages/schemas/src/types/logto-config/jwt-customizer.ts b/packages/schemas/src/types/logto-config/jwt-customizer.ts index 361a73c759c..4780373bc6b 100644 --- a/packages/schemas/src/types/logto-config/jwt-customizer.ts +++ b/packages/schemas/src/types/logto-config/jwt-customizer.ts @@ -10,6 +10,7 @@ import { type UserSsoIdentity, } from '../../db-entries/index.js'; import { mfaFactorsGuard, type MfaFactors } from '../../foundations/index.js'; +import { GrantType } from '../oidc-config.js'; import { scopeResponseGuard, type ScopeResponse } from '../scope.js'; import { userInfoGuard, type UserInfo } from '../user.js'; @@ -67,11 +68,23 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({ .array(), }) satisfies ZodType; +export const jwtCustomizerGrantContextGuard = z.object({ + type: z.literal(GrantType.TokenExchange), // Only support token exchange for now + subjectTokenContext: jsonObjectGuard, +}); + +export type JwtCustomizerGrantContext = z.infer; + export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard .extend({ // Use partial token guard since users customization may not rely on all fields. tokenSample: accessTokenPayloadGuard.partial().optional(), - contextSample: z.object({ user: jwtCustomizerUserContextGuard.partial() }).optional(), + contextSample: z + .object({ + user: jwtCustomizerUserContextGuard.partial(), + grant: jwtCustomizerGrantContextGuard.partial().optional(), + }) + .optional(), }) .strict();