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:
parent
add78b77a4
commit
977776d31f
9 changed files with 100 additions and 102 deletions
|
@ -17,7 +17,7 @@ function ErrorContent({ testResult }: Props) {
|
|||
)}
|
||||
{testResult.payload && (
|
||||
<pre>
|
||||
{'JWT Payload: \n'}
|
||||
{'Extra JWT claims: \n'}
|
||||
{testResult.payload}
|
||||
</pre>
|
||||
)}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}) `;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
]);
|
||||
|
Loading…
Reference in a new issue