Skip to content

Commit

Permalink
feat(console,schemas): add grant context to custom jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Jul 5, 2024
1 parent b9f821a commit 2ae998e
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'node:fs';
import {
accessTokenPayloadGuard,
clientCredentialsPayloadGuard,
jwtCustomizerGrantContextGuard,
jwtCustomizerUserContextGuard,
} from '@logto/schemas';
import prettier from 'prettier';
Expand All @@ -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',
Expand Down Expand Up @@ -43,6 +45,11 @@ const createJwtCustomizerTypeDefinitions = async () => {
'JwtCustomizerUserContext'
);

const jwtCustomizerGrantContextTypeDefinition = inferTsDefinitionFromZod(
jwtCustomizerGrantContextGuard,
'JwtCustomizerGrantContext'
);

const accessTokenPayloadTypeDefinition = inferTsDefinitionFromZod(
accessTokenPayloadGuard,
'AccessTokenPayload'
Expand All @@ -58,6 +65,8 @@ ${typeIdentifiers}
export const jwtCustomizerUserContextTypeDefinition = \`${jwtCustomizerUserContextTypeDefinition}\`;
export const jwtCustomizerGrantContextTypeDefinition = \`${jwtCustomizerGrantContextTypeDefinition}\`;
export const accessTokenPayloadTypeDefinition = \`${accessTokenPayloadTypeDefinition}\`;
export const clientCredentialsPayloadTypeDefinition = \`${clientCredentialsPayloadTypeDefinition}\`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
accessTokenPayloadTypeDefinition,
clientCredentialsPayloadTypeDefinition,
jwtCustomizerUserContextTypeDefinition,
jwtCustomizerGrantContextTypeDefinition,
} from '@/pages/CustomizeJwtDetails/utils/type-definitions';

import * as tabContentStyles from '../index.module.scss';
Expand Down Expand Up @@ -77,6 +78,24 @@ function InstructionTab({ isActive }: Props) {
/>
</GuideCard>
)}
{tokenType === LogtoJwtTokenKeyType.AccessToken && (
<GuideCard
name={CardType.GrantData}
isExpanded={expendCard === CardType.GrantData}
setExpanded={(expand) => {
setExpendCard(expand ? CardType.GrantData : undefined);
}}
>
<Editor
language="typescript"
className={styles.sampleCode}
value={jwtCustomizerGrantContextTypeDefinition}
height="400px"
theme="logto-dark"
options={typeDefinitionCodeEditorOptions}
/>
</GuideCard>
)}
<GuideCard
name={CardType.FetchExternalData}
isExpanded={expendCard === CardType.FetchExternalData}
Expand Down
21 changes: 16 additions & 5 deletions packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type {
AccessTokenPayload,
ClientCredentialsPayload,
JwtCustomizerUserContext,
import {
GrantType,
type AccessTokenPayload,
type ClientCredentialsPayload,
type JwtCustomizerUserContext,
type JwtCustomizerGrantContext,
} from '@logto/schemas';
import { type EditorProps } from '@monaco-editor/react';

Expand All @@ -27,6 +29,7 @@ declare interface CustomJwtClaims extends Record<string, any> {}
*/
declare type Context = {
user: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext};
grant?: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerGrantContext};
}
declare type Payload = {
Expand Down Expand Up @@ -199,8 +202,16 @@ const defaultUserContext: Partial<JwtCustomizerUserContext> = {
organizationRoles: [],
};

const defaultGrantContext: Partial<JwtCustomizerGrantContext> = {
type: GrantType.TokenExchange,
subjectTokenContext: {
foo: 'bar',
},
};

export const defaultUserTokenContextData = {
user: defaultUserContext,
grant: defaultGrantContext,
};

export const accessTokenPayloadTestModel: ModelSettings = {
Expand All @@ -223,6 +234,6 @@ export const userContextTestModel: ModelSettings = {
language: 'json',
icon: <UserFileIcon />,
name: 'user-token-context.json',
title: 'User data',
title: 'Context data',
defaultValue: JSON.stringify(defaultUserTokenContextData, null, 2),
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
accessTokenPayloadTypeDefinition,
clientCredentialsPayloadTypeDefinition,
jwtCustomizerUserContextTypeDefinition,
jwtCustomizerGrantContextTypeDefinition,
} from '@/consts/jwt-customizer-type-definition';

import { type JwtCustomizerForm } from '../type';
Expand All @@ -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}`;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -12,7 +13,7 @@ const accountId = 'some_account_id';
const validOidcContext: Partial<KoaContextWithOIDC['oidc']> = {
params: {
actor_token: 'some_actor_token',
actor_token_type: 'urn:ietf:params:oauth:token-type:access_token',
actor_token_type: TokenExchangeTokenType.AccessToken,
},
};

Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/oidc/grants/token-exchange/actor-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -35,5 +34,5 @@ export const handleActorToken = async (
new InvalidGrant('actor token must have openid scope')
);

return { accountId: actorToken.accountId };
return { actorId: actorToken.accountId };
};
4 changes: 3 additions & 1 deletion packages/core/src/oidc/grants/token-exchange/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -63,7 +65,7 @@ const validSubjectToken: SubjectToken = {
const validOidcContext: Partial<KoaContextWithOIDC['oidc']> = {
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,
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/oidc/grants/token-exchange/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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')
);

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/oidc/grants/token-exchange/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export const tokenExchangeActGuard = z.object({
});

export type TokenExchangeAct = z.infer<typeof tokenExchangeActGuard>;

export enum TokenExchangeTokenType {
AccessToken = 'urn:ietf:params:oauth:token-type:access_token',
}
26 changes: 12 additions & 14 deletions packages/core/src/oidc/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ',
Expand Down
15 changes: 14 additions & 1 deletion packages/schemas/src/types/logto-config/jwt-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -67,11 +68,23 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
.array(),
}) satisfies ZodType<JwtCustomizerUserContext>;

export const jwtCustomizerGrantContextGuard = z.object({
type: z.literal(GrantType.TokenExchange), // Only support token exchange for now
subjectTokenContext: jsonObjectGuard,
});

export type JwtCustomizerGrantContext = z.infer<typeof jwtCustomizerGrantContextGuard>;

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();

Expand Down

0 comments on commit 2ae998e

Please sign in to comment.