diff --git a/.changeset/spicy-cameras-sleep.md b/.changeset/spicy-cameras-sleep.md
new file mode 100644
index 000000000..d314e2e62
--- /dev/null
+++ b/.changeset/spicy-cameras-sleep.md
@@ -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 request, with a custom error message
+ return api.denyAccess('Access denied');
+}
+```
diff --git a/packages/console/scripts/custom-jwt-customizer-type-definition.ts b/packages/console/scripts/custom-jwt-customizer-type-definition.ts
index 8ac316026..130238494 100644
--- a/packages/console/scripts/custom-jwt-customizer-type-definition.ts
+++ b/packages/console/scripts/custom-jwt-customizer-type-definition.ts
@@ -12,10 +12,10 @@ import { CustomJwtApiContext } from '@logto/schemas';
*/
export const jwtCustomizerApiContextTypeDefinition = `type CustomJwtApiContext = {
/**
- * Reject the the current token exchange request.
+ * Reject the the current token request.
*
* @remarks
- * This function will reject the current token exchange request and throw
+ * This function will reject the current token request and throw
* an OIDC AccessDenied error to the client.
*
* @param {string} [message] - The custom error message.
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 bdb631011..1bb464771 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx
@@ -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,
@@ -138,24 +137,22 @@ function InstructionTab({ isActive }: Props) {
options={sampleCodeEditorOptions}
/>
- {isDevFeaturesEnabled && (
- {
- setExpendCard(expand ? CardType.ApiContext : undefined);
- }}
- >
-
-
- )}
+ {
+ setExpendCard(expand ? CardType.ApiContext : undefined);
+ }}
+ >
+
+
{t('jwt_claims.jwt_claims_description')}
);
diff --git a/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx b/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx
index e45d82a7b..116bd1d04 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx
+++ b/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx
@@ -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';
@@ -29,9 +28,7 @@ declare interface CustomJwtClaims extends Record {}
/** 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 = {
/**
@@ -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};
};`;
/**
@@ -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 = `/**
@@ -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 {};
}`;
@@ -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 {};
}`;
@@ -221,8 +204,7 @@ export const denyAccessCodeExample = `/**
*/
getCustomJwtClaims = async ({ api }) => {
// Conditionally deny access
- api.denyAccess('Access denied');
- return {};
+ return api.denyAccess('Access denied');
};`;
/**
diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index f9c120635..fd1ba8360 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -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';
@@ -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);
diff --git a/packages/core/src/oidc/extra-token-claims.ts b/packages/core/src/oidc/extra-token-claims.ts
index 0c85ec3b6..f8f14fb3d 100644
--- a/packages/core/src/oidc/extra-token-claims.ts
+++ b/packages/core/src/oidc/extra-token-claims.ts
@@ -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));
diff --git a/packages/integration-tests/src/__mocks__/jwt-customizer.ts b/packages/integration-tests/src/__mocks__/jwt-customizer.ts
index f92648116..4636cc093 100644
--- a/packages/integration-tests/src/__mocks__/jwt-customizer.ts
+++ b/packages/integration-tests/src/__mocks__/jwt-customizer.ts
@@ -59,8 +59,7 @@ export const accessTokenSampleScript = `const getCustomJwtClaims = async ({ toke
};`;
export const accessTokenAccessDeniedSampleScript = `const getCustomJwtClaims = async ({ token, context, environmentVariables, api }) => {
- api.denyAccess('You are not allowed to access this resource');
- return { test: 'foo'};
+ return api.denyAccess('You are not allowed to access this resource');
};`;
export const clientCredentialsSampleScript = `const getCustomJwtClaims = async ({ token, context, environmentVariables }) => {
@@ -68,6 +67,5 @@ export const clientCredentialsSampleScript = `const getCustomJwtClaims = async (
}`;
export const clientCredentialsAccessDeniedSampleScript = `const getCustomJwtClaims = async ({ token, context, environmentVariables, api }) => {
- api.denyAccess('You are not allowed to access this resource');
- return { test: 'foo'};
+ return api.denyAccess('You are not allowed to access this resource');
};`;
diff --git a/packages/integration-tests/src/tests/api/admin-user.personal-access-tokens.test.ts b/packages/integration-tests/src/tests/api/admin-user.personal-access-tokens.test.ts
index 8ffbe6af6..7cf27a3fb 100644
--- a/packages/integration-tests/src/tests/api/admin-user.personal-access-tokens.test.ts
+++ b/packages/integration-tests/src/tests/api/admin-user.personal-access-tokens.test.ts
@@ -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();
diff --git a/packages/integration-tests/src/tests/api/logto-config.test.ts b/packages/integration-tests/src/tests/api/logto-config.test.ts
index 0298985ea..3128e50f7 100644
--- a/packages/integration-tests/src/tests/api/logto-config.test.ts
+++ b/packages/integration-tests/src/tests/api/logto-config.test.ts
@@ -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,
@@ -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,
+ }
+ );
+ });
});
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 4d97d606a..2d8aef6f4 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
@@ -41,7 +41,7 @@ const jwt_claims = {
},
api_context: {
title: 'API context: access control',
- subtitle: 'Use `api.denyAccess` method to reject the token exchange request.',
+ subtitle: 'Use `api.denyAccess` method to reject the token request.',
},
fetch_external_data: {
title: 'Fetch external data',
diff --git a/packages/schemas/src/types/logto-config/jwt-customizer.ts b/packages/schemas/src/types/logto-config/jwt-customizer.ts
index 732e690f5..d8f25cffa 100644
--- a/packages/schemas/src/types/logto-config/jwt-customizer.ts
+++ b/packages/schemas/src/types/logto-config/jwt-customizer.ts
@@ -168,10 +168,10 @@ export type CustomJwtErrorBody = z.infer;
export type CustomJwtApiContext = {
/**
- * Reject the the current token exchange request.
+ * Reject the the current token request.
*
* @remarks
- * By calling this function, the current token exchange request will be rejected,
+ * By calling this function, the current token request will be rejected,
* and a OIDC `AccessDenied` error will be thrown to the client with the given message.
*
* @param message The message to be shown to the user.