0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console,core): remove custom token claims api context dev guard (#6553)

* feat(console,core): remove custom jwt api context dev guard

remove custom jwt api context dev guard

* fix(console,schemas,phrases): fix custom jwt token request phrases

fix custom jwt token request phrases

* chore: return denyAccess
return denyAccess
This commit is contained in:
simeng-li 2024-09-09 16:57:44 +08:00 committed by GitHub
parent 5f3c0691b5
commit b837efead6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 80 additions and 105 deletions

View file

@ -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');
}
```

View file

@ -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.

View file

@ -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}
/>
</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>
);

View file

@ -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<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 = {
/**
@ -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');
};`;
/**

View file

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

View file

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

View file

@ -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');
};`;

View file

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

View file

@ -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,
}
);
});
});

View file

@ -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',

View file

@ -168,10 +168,10 @@ export type CustomJwtErrorBody = z.infer<typeof customJwtErrorBodyGuard>;
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.