0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(core,schemas): make the jwt customizer script field mandatory (#5696)

* refactor(core,schemas): make the jwt customizer script field mandatory

make the jwt customizer script field mandatory

* fix(schemas): fix the alteration script

fix the alteration script

* fix(schemas): fix ut

fix ut
This commit is contained in:
simeng-li 2024-04-15 10:38:30 +08:00 committed by GitHub
parent c08feeb872
commit 559331d51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 38 deletions

View file

@ -155,11 +155,10 @@ describe('configs JWT customizer routes', () => {
jest.spyOn(mockCloudClient, 'post').mockResolvedValue(cloudConnectionResponse); jest.spyOn(mockCloudClient, 'post').mockResolvedValue(cloudConnectionResponse);
const payload: JwtCustomizerTestRequestBody = { const payload: JwtCustomizerTestRequestBody = {
tokenType: LogtoJwtTokenKeyType.AccessToken, tokenType: LogtoJwtTokenKeyType.ClientCredentials,
script: mockJwtCustomizerConfigForClientCredentials.value.script,
environmentVariables: mockJwtCustomizerConfigForClientCredentials.value.environmentVariables,
token: {}, token: {},
script: mockJwtCustomizerConfigForAccessToken.value.script,
environmentVariables: mockJwtCustomizerConfigForAccessToken.value.environmentVariables,
context: mockJwtCustomizerConfigForAccessToken.value.contextSample,
}; };
const response = await routeRequester.post('/configs/jwt-customizer/test').send(payload); const response = await routeRequester.post('/configs/jwt-customizer/test').send(payload);
@ -167,7 +166,7 @@ describe('configs JWT customizer routes', () => {
expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith( expect(mockLogtoConfigsLibrary.deployJwtCustomizerScript).toHaveBeenCalledWith(
tenantContext.cloudConnection, tenantContext.cloudConnection,
{ {
key: LogtoJwtTokenKey.AccessToken, key: LogtoJwtTokenKey.ClientCredentials,
value: payload, value: payload,
isTest: true, isTest: true,
} }

View file

@ -207,11 +207,6 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
router.post( router.post(
'/configs/jwt-customizer/test', '/configs/jwt-customizer/test',
koaGuard({ koaGuard({
/**
* Early throws when:
* 1. no `script` provided.
* 2. no `tokenSample` provided.
*/
body: jwtCustomizerTestRequestBodyGuard, body: jwtCustomizerTestRequestBodyGuard,
response: jsonObjectGuard, response: jsonObjectGuard,
status: [200, 400, 403, 422], status: [200, 400, 403, 422],

View file

@ -0,0 +1,23 @@
import { sql } from '@silverhand/slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
// We are making the jwt-customizer script field mandatory
// Delete the records in logto_configs where key is jwt.accessToken or jwt.clientCredentials and value jsonb's script field is undefined
up: async (pool) => {
await pool.query(
sql`
delete from logto_configs
where key in ('jwt.accessToken', 'jwt.clientCredentials')
and value->>'script' is null
`
);
},
down: async () => {
// No down script available, this is a non-reversible operation
// It is fine since we have not released this feature yet
},
};
export default alteration;

View file

@ -1,5 +1,5 @@
import { pick } from '@silverhand/essentials'; import { pick } from '@silverhand/essentials';
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
accessTokenJwtCustomizerGuard, accessTokenJwtCustomizerGuard,
@ -7,6 +7,8 @@ import {
} from './jwt-customizer.js'; } from './jwt-customizer.js';
const allFields = ['script', 'environmentVariables', 'contextSample', 'tokenSample'] as const; const allFields = ['script', 'environmentVariables', 'contextSample', 'tokenSample'] as const;
const requiredFields = ['script'] as const;
const optionalFields = ['environmentVariables', 'contextSample', 'tokenSample'] as const;
const testClientCredentialsTokenPayload = { const testClientCredentialsTokenPayload = {
script: '', script: '',
@ -39,14 +41,35 @@ const testAccessTokenPayload = {
}; };
describe('test token sample guard', () => { describe('test token sample guard', () => {
it.each(allFields)('should pass guard with any of the field not specified', (droppedField) => { it.each(optionalFields)(
'should pass guard with any of the optionalFields not specified',
(droppedField) => {
const resultAccessToken = accessTokenJwtCustomizerGuard.safeParse(
pick(testAccessTokenPayload, ...allFields.filter((field) => field !== droppedField))
);
if (!resultAccessToken.success) {
console.log('resultAccessToken.error', resultAccessToken.error);
}
expect(resultAccessToken.success).toBe(true);
const resultClientCredentials = clientCredentialsJwtCustomizerGuard.safeParse(
pick(
testClientCredentialsTokenPayload,
...allFields.filter((field) => field !== droppedField)
)
);
if (!resultClientCredentials.success) {
console.log('resultClientCredentials.error', resultClientCredentials.error);
}
expect(resultClientCredentials.success).toBe(true);
}
);
it.each(requiredFields)('should throw when required field is not specified', (droppedField) => {
const resultAccessToken = accessTokenJwtCustomizerGuard.safeParse( const resultAccessToken = accessTokenJwtCustomizerGuard.safeParse(
pick(testAccessTokenPayload, ...allFields.filter((field) => field !== droppedField)) pick(testAccessTokenPayload, ...allFields.filter((field) => field !== droppedField))
); );
if (!resultAccessToken.success) { expect(resultAccessToken.success).toBe(false);
console.log('resultAccessToken.error', resultAccessToken.error);
}
expect(resultAccessToken.success).toBe(true);
const resultClientCredentials = clientCredentialsJwtCustomizerGuard.safeParse( const resultClientCredentials = clientCredentialsJwtCustomizerGuard.safeParse(
pick( pick(
@ -54,10 +77,7 @@ describe('test token sample guard', () => {
...allFields.filter((field) => field !== droppedField) ...allFields.filter((field) => field !== droppedField)
) )
); );
if (!resultClientCredentials.success) { expect(resultClientCredentials.success).toBe(false);
console.log('resultClientCredentials.error', resultClientCredentials.error);
}
expect(resultClientCredentials.success).toBe(true);
}); });
it.each(allFields)( it.each(allFields)(

View file

@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { Roles, UserSsoIdentities, Organizations } from '../../db-entries/index.js'; import { Organizations, Roles, UserSsoIdentities } from '../../db-entries/index.js';
import { jsonObjectGuard, mfaFactorsGuard } from '../../foundations/index.js'; import { jsonObjectGuard, mfaFactorsGuard } from '../../foundations/index.js';
import { scopeResponseGuard } from '../scope.js'; import { scopeResponseGuard } from '../scope.js';
import { userInfoGuard } from '../user.js'; import { userInfoGuard } from '../user.js';
@ -32,13 +32,11 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>; export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
export const jwtCustomizerGuard = z export const jwtCustomizerGuard = z.object({
.object({ script: z.string(),
script: z.string(), environmentVariables: z.record(z.string()).optional(),
environmentVariables: z.record(z.string()), contextSample: jsonObjectGuard.optional(),
contextSample: jsonObjectGuard, });
})
.partial();
export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard
.extend({ .extend({
@ -66,25 +64,21 @@ export enum LogtoJwtTokenKeyType {
/** /**
* This guard is for the core JWT customizer testing API request body guard. * This guard is for the core JWT customizer testing API request body guard.
* Unlike the DB guard
*
* - rename the `tokenSample` to `token` and is required for testing.
* - rename the `contextSample` to `context` and is required for AccessToken testing.
*/ */
export const jwtCustomizerTestRequestBodyGuard = z.discriminatedUnion('tokenType', [ export const jwtCustomizerTestRequestBodyGuard = z.discriminatedUnion('tokenType', [
z.object({ z.object({
tokenType: z.literal(LogtoJwtTokenKeyType.AccessToken), tokenType: z.literal(LogtoJwtTokenKeyType.AccessToken),
...accessTokenJwtCustomizerGuard ...accessTokenJwtCustomizerGuard.pick({ environmentVariables: true, script: true }).shape,
.required({
script: true,
})
.pick({ environmentVariables: true, script: true }).shape,
token: accessTokenJwtCustomizerGuard.required().shape.tokenSample, token: accessTokenJwtCustomizerGuard.required().shape.tokenSample,
context: accessTokenJwtCustomizerGuard.required().shape.contextSample, context: accessTokenJwtCustomizerGuard.required().shape.contextSample,
}), }),
z.object({ z.object({
tokenType: z.literal(LogtoJwtTokenKeyType.ClientCredentials), tokenType: z.literal(LogtoJwtTokenKeyType.ClientCredentials),
...clientCredentialsJwtCustomizerGuard ...clientCredentialsJwtCustomizerGuard.pick({ environmentVariables: true, script: true }).shape,
.required({
script: true,
})
.pick({ environmentVariables: true, script: true }).shape,
token: clientCredentialsJwtCustomizerGuard.required().shape.tokenSample, token: clientCredentialsJwtCustomizerGuard.required().shape.tokenSample,
}), }),
]); ]);