Skip to content

Commit

Permalink
feat(console,core): remove custom jwt api context dev guard
Browse files Browse the repository at this point in the history
remove custom jwt api context dev guard
  • Loading branch information
simeng-li committed Sep 6, 2024
1 parent 69da43c commit 558e86a
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 94 deletions.
16 changes: 16 additions & 0 deletions .changeset/spicy-cameras-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@logto/console": minor
"@logto/core": minor
---

add access deny method to the custom token claims script

Introduce a new `api` parameter to the custom token claims script. This parameter is used to provide more access control context over the token exchange process.
Use `api.denyAccess()` to reject the token exchange request. Use this method to implement your own access control logics.

```javascript
const getCustomJwtClaims: async ({api}) => {
// Reject the token exchange request
api.denyAccess('Access denied');
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { isDevFeaturesEnabled } from '@/consts/env';
import { type JwtCustomizerForm } from '@/pages/CustomizeJwtDetails/type';
import {
denyAccessCodeExample,
Expand Down Expand Up @@ -138,24 +137,22 @@ function InstructionTab({ isActive }: Props) {
options={sampleCodeEditorOptions}
/>
</GuideCard>
{isDevFeaturesEnabled && (
<GuideCard
name={CardType.ApiContext}
isExpanded={expendCard === CardType.ApiContext}
setExpanded={(expand) => {
setExpendCard(expand ? CardType.ApiContext : undefined);
}}
>
<Editor
language="typescript"
className={styles.sampleCode}
value={denyAccessCodeExample}
height="240px"
theme="logto-dark"
options={sampleCodeEditorOptions}
/>
</GuideCard>
)}
<GuideCard
name={CardType.ApiContext}
isExpanded={expendCard === CardType.ApiContext}
setExpanded={(expand) => {
setExpendCard(expand ? CardType.ApiContext : undefined);
}}
>
<Editor
language="typescript"
className={styles.sampleCode}
value={denyAccessCodeExample}
height="240px"
theme="logto-dark"
options={sampleCodeEditorOptions}
/>
</GuideCard>
<div className={tabContentStyles.description}>{t('jwt_claims.jwt_claims_description')}</div>
</div>
);
Expand Down
31 changes: 7 additions & 24 deletions packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { type EditorProps } from '@monaco-editor/react';

import TokenFileIcon from '@/assets/icons/token-file-icon.svg?react';
import UserFileIcon from '@/assets/icons/user-file-icon.svg?react';
import { isDevFeaturesEnabled } from '@/consts/env.js';

import type { ModelSettings } from '../MainContent/MonacoCodeEditor/type.js';

Expand All @@ -29,9 +28,7 @@ declare interface CustomJwtClaims extends Record<string, any> {}
/** Logto internal data that can be used to pass additional information
*
* @param {${
JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext
}} user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} user - The user info associated with the token.
*/
declare type Context = {
/**
Expand Down Expand Up @@ -60,17 +57,12 @@ declare type Payload = {
* Custom environment variables.
*/
environmentVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables};
${
isDevFeaturesEnabled
? `
/**
/**
* Logto API context, provides callback methods for access control.
*
* @param {${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext}} api
*/
api: ${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext};`
: ''
}
api: ${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext};
};`;

/**
Expand All @@ -90,17 +82,12 @@ declare type Payload = {
* Custom environment variables.
*/
environmentVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables};
${
isDevFeaturesEnabled
? `
/**
/**
* Logto API context, callback methods for access control.
*
* @param {${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext}} api
*/
api: ${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext};`
: ''
}
api: ${JwtCustomizerTypeDefinitionKey.CustomJwtApiContext};
};`;

export const defaultAccessTokenJwtCustomizerCode = `/**
Expand All @@ -111,9 +98,7 @@ export const defaultAccessTokenJwtCustomizerCode = `/**
*
* @returns The custom claims.
*/
const getCustomJwtClaims = async ({ token, context, environmentVariables${
isDevFeaturesEnabled ? ', api' : ''
} }) => {
const getCustomJwtClaims = async ({ token, context, environmentVariables, api }) => {
return {};
}`;

Expand All @@ -125,9 +110,7 @@ export const defaultClientCredentialsJwtCustomizerCode = `/**
*
* @returns The custom claims.
*/
const getCustomJwtClaims = async ({ token, environmentVariables${
isDevFeaturesEnabled ? ', api' : ''
} }) => {
const getCustomJwtClaims = async ({ token, environmentVariables, api }) => {
return {};
}`;

Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/libraries/jwt-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type CustomJwtScriptPayload,
} from '@logto/schemas';
import { type ConsoleLog } from '@logto/shared';
import { assert, conditional, deduplicate, pick, pickState } from '@silverhand/essentials';
import { assert, deduplicate, pick, pickState } from '@silverhand/essentials';
import deepmerge from 'deepmerge';
import { ZodError, z } from 'zod';

Expand Down Expand Up @@ -53,17 +53,11 @@ export class JwtCustomizerLibrary {
// Convert errors to WithTyped client response error to share the error handling logic.
static async runScriptInLocalVm(data: CustomJwtFetcher) {
try {
// @ts-expect-error -- remove this when the dev feature is ready
const payload: CustomJwtScriptPayload = {
...(data.tokenType === LogtoJwtTokenKeyType.AccessToken
? pick(data, 'token', 'context', 'environmentVariables')
: pick(data, 'token', 'environmentVariables')),
...conditional(
// TODO: @simeng remove this when the dev feature is ready
EnvSet.values.isDevFeaturesEnabled && {
api: apiContext,
}
),
api: apiContext,
};

const result = await runScriptFunctionInLocalVm(data.script, 'getCustomJwtClaims', payload);
Expand Down
5 changes: 0 additions & 5 deletions packages/core/src/oidc/extra-token-claims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,6 @@ export const getExtraTokenClaimsForJwtCustomization = async (
},
});

// TODO: @simeng remove this once the feature is ready
if (!EnvSet.values.isDevFeaturesEnabled) {
return;
}

// If the error is an instance of `ResponseError`, we need to parse the customJwtError body to get the error code.
if (error instanceof ResponseError) {
const customJwtError = await trySafe(async () => parseCustomJwtResponseError(error));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
updatePersonalAccessToken,
} from '#src/api/admin-user.js';
import { createUserByAdmin } from '#src/helpers/index.js';
import { devFeatureTest, randomString } from '#src/utils.js';
import { randomString } from '#src/utils.js';

devFeatureTest.describe('personal access tokens', () => {
describe('personal access tokens', () => {
it('should throw error when creating PAT with existing name', async () => {
const user = await createUserByAdmin();
const name = randomString();
Expand Down
65 changes: 29 additions & 36 deletions packages/integration-tests/src/tests/api/logto-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
testJwtCustomizer,
} from '#src/api/index.js';
import { expectRejects } from '#src/helpers/index.js';
import { devFeatureTest } from '#src/utils.js';

const defaultAdminConsoleConfig: AdminConsoleData = {
signInExperienceCustomized: false,
Expand Down Expand Up @@ -272,40 +271,34 @@ describe('logto config', () => {
expect(testResult).toMatchObject(clientCredentialsJwtCustomizerPayload.environmentVariables);
});

devFeatureTest.it(
'should throw access denied error when calling the denyAccess api in the script',
async () => {
await expectRejects(
testJwtCustomizer({
tokenType: LogtoJwtTokenKeyType.AccessToken,
token: accessTokenJwtCustomizerPayload.tokenSample,
context: accessTokenJwtCustomizerPayload.contextSample,
script: accessTokenAccessDeniedSampleScript,
environmentVariables: accessTokenJwtCustomizerPayload.environmentVariables,
}),
{
code: 'jwt_customizer.general',
status: 403,
}
);
}
);
it('should throw access denied error when calling the denyAccess api in the script', async () => {
await expectRejects(
testJwtCustomizer({
tokenType: LogtoJwtTokenKeyType.AccessToken,
token: accessTokenJwtCustomizerPayload.tokenSample,
context: accessTokenJwtCustomizerPayload.contextSample,
script: accessTokenAccessDeniedSampleScript,
environmentVariables: accessTokenJwtCustomizerPayload.environmentVariables,
}),
{
code: 'jwt_customizer.general',
status: 403,
}
);
});

devFeatureTest.it(
'should throw access denied error when calling the denyAccess api in the script',
async () => {
await expectRejects(
testJwtCustomizer({
tokenType: LogtoJwtTokenKeyType.ClientCredentials,
token: clientCredentialsJwtCustomizerPayload.tokenSample,
script: clientCredentialsAccessDeniedSampleScript,
environmentVariables: clientCredentialsJwtCustomizerPayload.environmentVariables,
}),
{
code: 'jwt_customizer.general',
status: 403,
}
);
}
);
it('should throw access denied error when calling the denyAccess api in the script', async () => {
await expectRejects(
testJwtCustomizer({
tokenType: LogtoJwtTokenKeyType.ClientCredentials,
token: clientCredentialsJwtCustomizerPayload.tokenSample,
script: clientCredentialsAccessDeniedSampleScript,
environmentVariables: clientCredentialsJwtCustomizerPayload.environmentVariables,
}),
{
code: 'jwt_customizer.general',
status: 403,
}
);
});
});

0 comments on commit 558e86a

Please sign in to comment.