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

chore(core,console): update error handling of testing custom JWT

This commit is contained in:
Darcy Ye 2024-04-01 15:17:45 +08:00
parent add78b77a4
commit 977776d31f
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
9 changed files with 100 additions and 102 deletions

View file

@ -17,7 +17,7 @@ function ErrorContent({ testResult }: Props) {
)}
{testResult.payload && (
<pre>
{'JWT Payload: \n'}
{'Extra JWT claims: \n'}
{testResult.payload}
</pre>
)}

View file

@ -10,6 +10,7 @@ import { formatFormDataToTestRequestPayload } from '@/pages/CustomizeJwtDetails/
const testEndpointPath = 'api/configs/jwt-customizer/test';
const jwtCustomizerGeneralErrorCode = 'jwt_customizer.general';
const apiInvalidInputErrorCode = 'guard.invalid_input';
export type TestResultData = {
error?: string;
@ -35,6 +36,8 @@ const useTestHandler = () => {
if (error instanceof HTTPError) {
const { response } = error;
const metadata = await response.clone().json<RequestErrorBody>();
// Get error message from cloud connection client.
if (metadata.code === jwtCustomizerGeneralErrorCode) {
const result = z.object({ message: z.string() }).safeParse(metadata.data);
if (result.success) {
@ -44,6 +47,22 @@ const useTestHandler = () => {
return;
}
}
/**
* Get error message when the API request violates the request guard.
* Find details on the implementation of:
* 1. `RequestError`
* 2. `koaGuard`
*/
if (metadata.code === apiInvalidInputErrorCode) {
const result = z.string().safeParse(metadata.details);
if (result.success) {
setTestResult({
error: result.data,
});
return;
}
}
}
setTestResult({

View file

@ -209,20 +209,20 @@ export const defaultClientCredentialsPayload: ClientCredentialsPayload = {
const defaultUserContext: Partial<JwtCustomizerUserContext> = {
id: '123',
name: 'Foo Bar',
roles: [],
avatar: 'https://example.com/avatar.png',
profile: {},
username: 'foo',
customData: {},
identities: {},
primaryEmail: 'foo@logto.io',
primaryPhone: '+1234567890',
name: 'Foo Bar',
avatar: 'https://example.com/avatar.png',
customData: {},
identities: {},
profile: {},
applicationId: 'my-app',
organizations: [],
ssoIdentities: [],
organizationRoles: [],
mfaVerificationFactors: [],
roles: [],
organizations: [],
organizationRoles: [],
};
export const defaultUserTokenContextData = {

View file

@ -1,4 +1,4 @@
import { LogtoJwtTokenPath, type AccessTokenJwtCustomizer } from '@logto/schemas';
import { LogtoJwtTokenPath, type AccessTokenJwtCustomizer, type Json } from '@logto/schemas';
import type { JwtCustomizer, JwtCustomizerForm } from '../type';
@ -36,7 +36,7 @@ const formatEnvVariablesFormDataToRequest = (
return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
};
const formatSampleCodeJsonToString = (sampleJson?: AccessTokenJwtCustomizer['contextSample']) => {
const formatSampleCodeJsonToString = (sampleJson?: Json) => {
if (!sampleJson) {
return;
}
@ -106,15 +106,12 @@ export const formatFormDataToTestRequestPayload = ({
}: JwtCustomizerForm) => {
return {
tokenType,
payload: {
script,
envVars: formatEnvVariablesFormDataToRequest(environmentVariables),
tokenSample:
formatSampleCodeStringToJson(testSample.tokenSample) ??
defaultValues[tokenType].tokenSample,
contextSample:
formatSampleCodeStringToJson(testSample.contextSample) ??
defaultValues[tokenType].contextSample,
},
script,
envVars: formatEnvVariablesFormDataToRequest(environmentVariables),
token:
formatSampleCodeStringToJson(testSample.tokenSample) ?? defaultValues[tokenType].tokenSample,
context:
formatSampleCodeStringToJson(testSample.contextSample) ??
defaultValues[tokenType].contextSample,
};
};

View file

@ -5,7 +5,7 @@ import { conditional, pick } from '@silverhand/essentials';
import i18next from 'i18next';
import { ZodError } from 'zod';
const formatZodError = ({ issues }: ZodError): string[] =>
export const formatZodError = ({ issues }: ZodError): string[] =>
issues.map((issue) => {
const base = `Error in key path "${issue.path.map(String).join('.')}": (${issue.code}) `;

View file

@ -7,49 +7,16 @@ import {
adminTenantId,
jwtCustomizerConfigsGuard,
jwtCustomizerTestRequestBodyGuard,
type JwtCustomizerTestRequestBody,
type CustomJwtFetcher,
} from '@logto/schemas';
import { ResponseError } from '@withtyped/client';
import { z } from 'zod';
import { ZodError, z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import RequestError, { formatZodError } from '#src/errors/RequestError/index.js';
import koaGuard, { parse } from '#src/middleware/koa-guard.js';
import type { AuthedRouter, RouterInitArgs } from '../types.js';
/**
* Transpile the request body of the JWT customizer test API to the request body of the Cloud JWT customizer test API.
*
* @param body Core JWT customizer test API request body.
* @returns Request body of the Cloud JWT customizer test API.
*/
const transpileJwtCustomizerTestRequestBody = (
body: JwtCustomizerTestRequestBody
): CustomJwtFetcher => {
const { tokenType, payload } = body;
/**
* We have to deal with the `tokenType` and `payload` at the same time since they are put together as one of the discriminated union type.
* Otherwise the type inference will not work as expected.
*/
if (tokenType === LogtoJwtTokenPath.AccessToken) {
const { tokenSample: token, contextSample: context, ...rest } = payload;
return {
tokenType,
token,
context,
...rest,
};
}
const { tokenSample: token, contextSample, ...rest } = payload;
return {
tokenType,
token,
...rest,
};
};
const getJwtTokenKeyAndBody = (tokenPath: LogtoJwtTokenPath, body: unknown) => {
if (tokenPath === LogtoJwtTokenPath.AccessToken) {
return {
@ -197,10 +164,14 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
try {
ctx.body = await client.post(`/api/services/custom-jwt`, {
body: transpileJwtCustomizerTestRequestBody(body),
body,
});
} catch (error: unknown) {
/**
* All APIs should throw `RequestError` instead of `Error`.
* In the admin console, we caught the error and recognized the error with the code `jwt_customizer.general`,
* and then we extract and show the error message to the user.
*
* `ResponseError` comes from `@withtyped/client` and all `logto/core` API returns error in the
* format of `RequestError`, we manually transform it here to keep the error format consistent.
*/
@ -209,6 +180,13 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
throw new RequestError({ code: 'jwt_customizer.general', status: 422 }, { message });
}
if (error instanceof ZodError) {
throw new RequestError(
{ code: 'jwt_customizer.general', status: 422 },
{ message: formatZodError(error) }
);
}
throw error;
}

View file

@ -26,5 +26,4 @@ export * from './tenant.js';
export * from './tenant-organization.js';
export * from './mapi-proxy.js';
export * from './consent.js';
export * from './jwt-customizer.js';
export * from './onboarding.js';

View file

@ -1,11 +1,15 @@
import type { ZodType } from 'zod';
import { z } from 'zod';
import { jsonObjectGuard } from '../../foundations/index.js';
import { accessTokenPayloadGuard, clientCredentialsPayloadGuard } from './oidc-provider.js';
import {
type AccessTokenJwtCustomizer,
type ClientCredentialsJwtCustomizer,
accessTokenJwtCustomizerGuard,
clientCredentialsJwtCustomizerGuard,
} from './jwt-customizer.js';
export * from './oidc-provider.js';
export * from './jwt-customizer.js';
/**
* Logto OIDC signing key types, used mainly in REST API routes.
@ -56,28 +60,6 @@ export enum LogtoJwtTokenKey {
ClientCredentials = 'jwt.clientCredentials',
}
export const jwtCustomizerGuard = z
.object({
script: z.string(),
envVars: z.record(z.string()),
contextSample: jsonObjectGuard,
})
.partial();
export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard.extend({
// Use partial token guard since users customization may not rely on all fields.
tokenSample: accessTokenPayloadGuard.partial().optional(),
});
export type AccessTokenJwtCustomizer = z.infer<typeof accessTokenJwtCustomizerGuard>;
export const clientCredentialsJwtCustomizerGuard = jwtCustomizerGuard.extend({
// Use partial token guard since users customization may not rely on all fields.
tokenSample: clientCredentialsPayloadGuard.partial().optional(),
});
export type ClientCredentialsJwtCustomizer = z.infer<typeof clientCredentialsJwtCustomizerGuard>;
export type JwtCustomizerType = {
[LogtoJwtTokenKey.AccessToken]: AccessTokenJwtCustomizer;
[LogtoJwtTokenKey.ClientCredentials]: ClientCredentialsJwtCustomizer;

View file

@ -1,15 +1,11 @@
import { z } from 'zod';
import { Roles, UserSsoIdentities, Organizations } from '../db-entries/index.js';
import { jsonObjectGuard, mfaFactorsGuard } from '../foundations/index.js';
import { Roles, UserSsoIdentities, Organizations } from '../../db-entries/index.js';
import { jsonObjectGuard, mfaFactorsGuard } from '../../foundations/index.js';
import { scopeResponseGuard } from '../scope.js';
import { userInfoGuard } from '../user.js';
import {
jwtCustomizerGuard,
accessTokenJwtCustomizerGuard,
clientCredentialsJwtCustomizerGuard,
} from './logto-config/index.js';
import { scopeResponseGuard } from './scope.js';
import { userInfoGuard } from './user.js';
import { accessTokenPayloadGuard, clientCredentialsPayloadGuard } from './oidc-provider.js';
export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
ssoIdentities: UserSsoIdentities.guard
@ -36,6 +32,29 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
export const jwtCustomizerGuard = z
.object({
script: z.string(),
envVars: z.record(z.string()),
contextSample: jsonObjectGuard,
})
.partial();
export const accessTokenJwtCustomizerGuard = jwtCustomizerGuard.extend({
// Use partial token guard since users customization may not rely on all fields.
tokenSample: accessTokenPayloadGuard.partial().optional(),
contextSample: z.object({ user: jwtCustomizerUserContextGuard.partial() }),
});
export type AccessTokenJwtCustomizer = z.infer<typeof accessTokenJwtCustomizerGuard>;
export const clientCredentialsJwtCustomizerGuard = jwtCustomizerGuard.extend({
// Use partial token guard since users customization may not rely on all fields.
tokenSample: clientCredentialsPayloadGuard.partial().optional(),
});
export type ClientCredentialsJwtCustomizer = z.infer<typeof clientCredentialsJwtCustomizerGuard>;
export enum LogtoJwtTokenPath {
AccessToken = 'access-token',
ClientCredentials = 'client-credentials',
@ -47,18 +66,22 @@ export enum LogtoJwtTokenPath {
export const jwtCustomizerTestRequestBodyGuard = z.discriminatedUnion('tokenType', [
z.object({
tokenType: z.literal(LogtoJwtTokenPath.AccessToken),
payload: accessTokenJwtCustomizerGuard.required({
script: true,
tokenSample: true,
contextSample: true,
}),
...accessTokenJwtCustomizerGuard
.required({
script: true,
})
.pick({ envVars: true, script: true }).shape,
token: accessTokenJwtCustomizerGuard.required().shape.tokenSample,
context: accessTokenJwtCustomizerGuard.required().shape.contextSample,
}),
z.object({
tokenType: z.literal(LogtoJwtTokenPath.ClientCredentials),
payload: clientCredentialsJwtCustomizerGuard.required({
script: true,
tokenSample: true,
}),
...clientCredentialsJwtCustomizerGuard
.required({
script: true,
})
.pick({ envVars: true, script: true }).shape,
token: clientCredentialsJwtCustomizerGuard.required().shape.tokenSample,
}),
]);