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:
parent
c08feeb872
commit
559331d51e
5 changed files with 69 additions and 38 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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;
|
|
@ -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)(
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Reference in a new issue