From 118de630490041e5b5061150720c1f3dfa9be34d Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Mon, 4 Mar 2024 15:13:50 +0800 Subject: [PATCH 1/4] feat(core): add GET /configs/jwt-customizer API --- packages/core/src/queries/logto-config.ts | 10 +++++++ .../core/src/routes/logto-config.openapi.json | 19 ++++++++++++ packages/core/src/routes/logto-config.test.ts | 11 ++++++- packages/core/src/routes/logto-config.ts | 20 +++++++++++++ .../integration-tests/src/api/logto-config.ts | 5 ++++ .../src/tests/api/logto-config.test.ts | 29 +++++++++++++++---- 6 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/core/src/queries/logto-config.ts b/packages/core/src/queries/logto-config.ts index e7a68528e..83757ae9f 100644 --- a/packages/core/src/queries/logto-config.ts +++ b/packages/core/src/queries/logto-config.ts @@ -67,6 +67,15 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => { ` ); + const getJwtCustomizer = async (key: T) => + pool.one<{ value: JwtCustomizerType[T] }>( + sql` + select ${fields.value} + from ${table} + where ${fields.key} = ${key} + ` + ); + return { getAdminConsoleConfig, updateAdminConsoleConfig, @@ -74,5 +83,6 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => { getRowsByKeys, updateOidcConfigsByKey, upsertJwtCustomizer, + getJwtCustomizer, }; }; diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index 63d0e6d96..b0839b78f 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -149,6 +149,25 @@ "description": "The request body is invalid." } } + }, + "get": { + "summary": "Get JWT customizer", + "description": "Get the JWT customizer for the given token type.", + "parameters": [ + { + "in": "path", + "name": "tokenType", + "description": "The token type to get the JWT customizer for." + } + ], + "responses": { + "200": { + "description": "The JWT customizer." + }, + "404": { + "description": "The JWT customizer does not exist." + } + } } } } diff --git a/packages/core/src/routes/logto-config.test.ts b/packages/core/src/routes/logto-config.test.ts index ac4fc2e4a..14561390d 100644 --- a/packages/core/src/routes/logto-config.test.ts +++ b/packages/core/src/routes/logto-config.test.ts @@ -54,7 +54,7 @@ const logtoConfigQueries = { }), updateOidcConfigsByKey: jest.fn(), getRowsByKeys: jest.fn(async () => mockLogtoConfigRows), - // UpsertJwtCustomizer: jest.fn(), + getJwtCustomizer: jest.fn(), }; const logtoConfigLibraries = { @@ -266,4 +266,13 @@ describe('configs routes', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value); }); + + it('GET /configs/jwt-customizer/:tokenType should return the record', async () => { + logtoConfigQueries.getJwtCustomizer.mockResolvedValueOnce( + mockJwtCustomizerConfigForAccessToken + ); + const response = await routeRequester.get('/configs/jwt-customizer/access-token'); + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value); + }); }); diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 965a1a62a..3db4e3170 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -240,4 +240,24 @@ export default function logtoConfigRoutes( return next(); } ); + + router.get( + '/configs/jwt-customizer/:tokenTypePath', + koaGuard({ + params: z.object({ + tokenTypePath: z.nativeEnum(LogtoJwtTokenPath), + }), + response: jwtCustomizerAccessTokenGuard.or(jwtCustomizerClientCredentialsGuard), + status: [200, 404], + }), + async (ctx, next) => { + const { + params: { tokenTypePath }, + } = ctx.guard; + + const { value } = await getJwtCustomizer(getLogtoJwtTokenKey(tokenTypePath)); + ctx.body = value; + return next(); + } + ); } diff --git a/packages/integration-tests/src/api/logto-config.ts b/packages/integration-tests/src/api/logto-config.ts index 9a0d6e319..ed2e869b3 100644 --- a/packages/integration-tests/src/api/logto-config.ts +++ b/packages/integration-tests/src/api/logto-config.ts @@ -40,3 +40,8 @@ export const upsertJwtCustomizer = async ( authedAdminApi .put(`configs/jwt-customizer/${keyTypePath}`, { json: value }) .json(); + +export const getJwtCustomizer = async (keyTypePath: 'access-token' | 'client-credentials') => + authedAdminApi + .get(`configs/jwt-customizer/${keyTypePath}`) + .json(); 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 0ce00c82f..b430545a4 100644 --- a/packages/integration-tests/src/tests/api/logto-config.test.ts +++ b/packages/integration-tests/src/tests/api/logto-config.test.ts @@ -11,6 +11,7 @@ import { rotateOidcKeys, updateAdminConsoleConfig, upsertJwtCustomizer, + getJwtCustomizer, } from '#src/api/index.js'; import { expectRejects } from '#src/helpers/index.js'; @@ -125,7 +126,7 @@ describe('admin console sign-in experience', () => { expect(privateKeys2[1]?.id).toBe(privateKeys[0]?.id); }); - it('should successfully add a new JWT customizer', async () => { + it('should successfully POST and GET a JWT customizer (access token)', async () => { const accessTokenJwtCustomizerPayload = { script: '', envVars: {}, @@ -136,11 +137,11 @@ describe('admin console sign-in experience', () => { }, }, }; - const clientCredentialsJwtCustomizerPayload = { - ...accessTokenJwtCustomizerPayload, - contextSample: {}, - }; + await expectRejects(getJwtCustomizer('access-token'), { + code: 'entity.not_found', + statusCode: 404, + }); const accessToken = await upsertJwtCustomizer('access-token', accessTokenJwtCustomizerPayload); expect(accessToken).toMatchObject(accessTokenJwtCustomizerPayload); const newAccessTokenJwtCustomizerPayload = { @@ -152,7 +153,22 @@ describe('admin console sign-in experience', () => { newAccessTokenJwtCustomizerPayload ); expect(updatedAccessToken).toMatchObject(newAccessTokenJwtCustomizerPayload); + await expect(getJwtCustomizer('access-token')).resolves.toMatchObject( + newAccessTokenJwtCustomizerPayload + ); + }); + it('should successfully POST and GET a JWT customizer (client credentials)', async () => { + const clientCredentialsJwtCustomizerPayload = { + script: '', + envVars: {}, + contextSample: {}, + }; + + await expectRejects(getJwtCustomizer('client-credentials'), { + code: 'entity.not_found', + statusCode: 404, + }); const clientCredentials = await upsertJwtCustomizer( 'client-credentials', clientCredentialsJwtCustomizerPayload @@ -167,5 +183,8 @@ describe('admin console sign-in experience', () => { newClientCredentialsJwtCustomizerPayload ); expect(updatedClientCredentials).toMatchObject(newClientCredentialsJwtCustomizerPayload); + await expect(getJwtCustomizer('client-credentials')).resolves.toMatchObject( + clientCredentialsJwtCustomizerPayload + ); }); }); From 754d425a60e249f33992d4a7e8529d966880ee03 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 6 Mar 2024 13:10:15 +0800 Subject: [PATCH 2/4] chore(core,test): update tests and refactor getJwtCustomizer query --- packages/core/src/queries/logto-config.ts | 29 ++++++++++++------- .../core/src/routes/logto-config.openapi.json | 2 +- .../src/tests/api/logto-config.test.ts | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/src/queries/logto-config.ts b/packages/core/src/queries/logto-config.ts index 83757ae9f..ff0bf3544 100644 --- a/packages/core/src/queries/logto-config.ts +++ b/packages/core/src/queries/logto-config.ts @@ -1,5 +1,5 @@ import { - type jwtCustomizerConfigGuard, + jwtCustomizerConfigGuard, LogtoTenantConfigKey, LogtoConfigs, type AdminConsoleData, @@ -12,7 +12,9 @@ import { import { convertToIdentifiers } from '@logto/shared'; import type { CommonQueryMethods } from 'slonik'; import { sql } from 'slonik'; -import type { z } from 'zod'; +import { z } from 'zod'; + +import RequestError from '#src/errors/RequestError/index.js'; const { table, fields } = convertToIdentifiers(LogtoConfigs); @@ -67,14 +69,21 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => { ` ); - const getJwtCustomizer = async (key: T) => - pool.one<{ value: JwtCustomizerType[T] }>( - sql` - select ${fields.value} - from ${table} - where ${fields.key} = ${key} - ` - ); + const getJwtCustomizer = async (key: T) => { + const { rows } = await getRowsByKeys([key]); + + // If the record does not exist (`rows` is empty) + if (rows.length === 0) { + throw new RequestError({ + code: 'entity.not_exists', + name: table, + id: key, + status: 404, + }); + } + + return z.object({ value: jwtCustomizerConfigGuard[key] }).parse(rows[0]); + }; return { getAdminConsoleConfig, diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index b0839b78f..42cec335a 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -156,7 +156,7 @@ "parameters": [ { "in": "path", - "name": "tokenType", + "name": "tokenTypePath", "description": "The token type to get the JWT customizer for." } ], 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 b430545a4..76a14ce62 100644 --- a/packages/integration-tests/src/tests/api/logto-config.test.ts +++ b/packages/integration-tests/src/tests/api/logto-config.test.ts @@ -184,7 +184,7 @@ describe('admin console sign-in experience', () => { ); expect(updatedClientCredentials).toMatchObject(newClientCredentialsJwtCustomizerPayload); await expect(getJwtCustomizer('client-credentials')).resolves.toMatchObject( - clientCredentialsJwtCustomizerPayload + newClientCredentialsJwtCustomizerPayload ); }); }); From e32775400849e2ab6e110cd12520923072a79736 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 6 Mar 2024 14:16:43 +0800 Subject: [PATCH 3/4] fix(test): fix tests --- packages/integration-tests/src/tests/api/logto-config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 76a14ce62..d1b066e2e 100644 --- a/packages/integration-tests/src/tests/api/logto-config.test.ts +++ b/packages/integration-tests/src/tests/api/logto-config.test.ts @@ -139,7 +139,7 @@ describe('admin console sign-in experience', () => { }; await expectRejects(getJwtCustomizer('access-token'), { - code: 'entity.not_found', + code: 'entity.not_exists', statusCode: 404, }); const accessToken = await upsertJwtCustomizer('access-token', accessTokenJwtCustomizerPayload); @@ -166,7 +166,7 @@ describe('admin console sign-in experience', () => { }; await expectRejects(getJwtCustomizer('client-credentials'), { - code: 'entity.not_found', + code: 'entity.not_exists', statusCode: 404, }); const clientCredentials = await upsertJwtCustomizer( From 5d55776d60fe871698fbdc1aa470b9b343a99db3 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 6 Mar 2024 19:42:10 +0800 Subject: [PATCH 4/4] refactor(core): refactor --- .../src/libraries/cloud-connection.test.ts | 1 + packages/core/src/libraries/logto-config.ts | 22 ++++++++++++++++++- .../sign-in-experience/index.test.ts | 1 + packages/core/src/routes/logto-config.test.ts | 4 ++-- packages/core/src/routes/logto-config.ts | 9 +++++--- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/core/src/libraries/cloud-connection.test.ts b/packages/core/src/libraries/cloud-connection.test.ts index 6c3289c19..3bb7b09a7 100644 --- a/packages/core/src/libraries/cloud-connection.test.ts +++ b/packages/core/src/libraries/cloud-connection.test.ts @@ -35,6 +35,7 @@ const logtoConfigs: LogtoConfigLibrary = { }), getOidcConfigs: jest.fn(), upsertJwtCustomizer: jest.fn(), + getJwtCustomizer: jest.fn(), }; describe('getAccessToken()', () => { diff --git a/packages/core/src/libraries/logto-config.ts b/packages/core/src/libraries/logto-config.ts index dc7882b0d..11093467a 100644 --- a/packages/core/src/libraries/logto-config.ts +++ b/packages/core/src/libraries/logto-config.ts @@ -1,4 +1,5 @@ import { + LogtoConfigs, cloudApiIndicator, cloudConnectionDataGuard, logtoOidcConfigGuard, @@ -6,13 +7,16 @@ import { jwtCustomizerConfigGuard, } from '@logto/schemas'; import type { LogtoOidcConfigType, LogtoJwtTokenKey } from '@logto/schemas'; +import { convertToIdentifiers } from '@logto/shared'; import chalk from 'chalk'; import { z, ZodError } from 'zod'; +import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; import { consoleLog } from '#src/utils/console.js'; export type LogtoConfigLibrary = ReturnType; +const { table } = convertToIdentifiers(LogtoConfigs); export const createLogtoConfigLibrary = ({ logtoConfigs: { @@ -77,5 +81,21 @@ export const createLogtoConfigLibrary = ({ }; }; - return { getOidcConfigs, getCloudConnectionData, upsertJwtCustomizer }; + const getJwtCustomizer = async (key: T) => { + const { rows } = await getRowsByKeys([key]); + + // If the record does not exist (`rows` is empty) + if (rows.length === 0) { + throw new RequestError({ + code: 'entity.not_exists', + name: table, + id: key, + status: 404, + }); + } + + return z.object({ value: jwtCustomizerConfigGuard[key] }).parse(rows[0]); + }; + + return { getOidcConfigs, getCloudConnectionData, upsertJwtCustomizer, getJwtCustomizer }; }; diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index 04c61c70b..2efdd1c00 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -58,6 +58,7 @@ const cloudConnection = createCloudConnectionLibrary({ }), getOidcConfigs: jest.fn(), upsertJwtCustomizer: jest.fn(), + getJwtCustomizer: jest.fn(), }); const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); diff --git a/packages/core/src/routes/logto-config.test.ts b/packages/core/src/routes/logto-config.test.ts index 14561390d..286d7a9b3 100644 --- a/packages/core/src/routes/logto-config.test.ts +++ b/packages/core/src/routes/logto-config.test.ts @@ -54,7 +54,6 @@ const logtoConfigQueries = { }), updateOidcConfigsByKey: jest.fn(), getRowsByKeys: jest.fn(async () => mockLogtoConfigRows), - getJwtCustomizer: jest.fn(), }; const logtoConfigLibraries = { @@ -63,6 +62,7 @@ const logtoConfigLibraries = { [LogtoOidcConfigKey.CookieKeys]: mockCookieKeys, })), upsertJwtCustomizer: jest.fn(), + getJwtCustomizer: jest.fn(), }; const settingRoutes = await pickDefault(import('./logto-config.js')); @@ -268,7 +268,7 @@ describe('configs routes', () => { }); it('GET /configs/jwt-customizer/:tokenType should return the record', async () => { - logtoConfigQueries.getJwtCustomizer.mockResolvedValueOnce( + logtoConfigLibraries.getJwtCustomizer.mockResolvedValueOnce( mockJwtCustomizerConfigForAccessToken ); const response = await routeRequester.get('/configs/jwt-customizer/access-token'); diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 3db4e3170..413f0d4e9 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -83,7 +83,7 @@ export default function logtoConfigRoutes( ) { const { getAdminConsoleConfig, getRowsByKeys, updateAdminConsoleConfig, updateOidcConfigsByKey } = queries.logtoConfigs; - const { getOidcConfigs, upsertJwtCustomizer } = logtoConfigs; + const { getOidcConfigs, upsertJwtCustomizer, getJwtCustomizer } = logtoConfigs; router.get( '/configs/admin-console', @@ -254,8 +254,11 @@ export default function logtoConfigRoutes( const { params: { tokenTypePath }, } = ctx.guard; - - const { value } = await getJwtCustomizer(getLogtoJwtTokenKey(tokenTypePath)); + const { value } = await getJwtCustomizer( + tokenTypePath === LogtoJwtTokenPath.AccessToken + ? LogtoJwtTokenKey.AccessToken + : LogtoJwtTokenKey.ClientCredentials + ); ctx.body = value; return next(); }