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:
parent
5f3c0691b5
commit
b837efead6
11 changed files with 80 additions and 105 deletions
16
.changeset/spicy-cameras-sleep.md
Normal file
16
.changeset/spicy-cameras-sleep.md
Normal 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');
|
||||
}
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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,7 +137,6 @@ function InstructionTab({ isActive }: Props) {
|
|||
options={sampleCodeEditorOptions}
|
||||
/>
|
||||
</GuideCard>
|
||||
{isDevFeaturesEnabled && (
|
||||
<GuideCard
|
||||
name={CardType.ApiContext}
|
||||
isExpanded={expendCard === CardType.ApiContext}
|
||||
|
@ -155,7 +153,6 @@ function InstructionTab({ isActive }: Props) {
|
|||
options={sampleCodeEditorOptions}
|
||||
/>
|
||||
</GuideCard>
|
||||
)}
|
||||
<div className={tabContentStyles.description}>{t('jwt_claims.jwt_claims_description')}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
};`;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const result = await runScriptFunctionInLocalVm(data.script, 'getCustomJwtClaims', payload);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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');
|
||||
};`;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,9 +271,7 @@ describe('logto config', () => {
|
|||
expect(testResult).toMatchObject(clientCredentialsJwtCustomizerPayload.environmentVariables);
|
||||
});
|
||||
|
||||
devFeatureTest.it(
|
||||
'should throw access denied error when calling the denyAccess api in the script',
|
||||
async () => {
|
||||
it('should throw access denied error when calling the denyAccess api in the script', async () => {
|
||||
await expectRejects(
|
||||
testJwtCustomizer({
|
||||
tokenType: LogtoJwtTokenKeyType.AccessToken,
|
||||
|
@ -288,12 +285,9 @@ describe('logto config', () => {
|
|||
status: 403,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
devFeatureTest.it(
|
||||
'should throw access denied error when calling the denyAccess api in the script',
|
||||
async () => {
|
||||
it('should throw access denied error when calling the denyAccess api in the script', async () => {
|
||||
await expectRejects(
|
||||
testJwtCustomizer({
|
||||
tokenType: LogtoJwtTokenKeyType.ClientCredentials,
|
||||
|
@ -306,6 +300,5 @@ describe('logto config', () => {
|
|||
status: 403,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue