# How to Customize JWT Tokens Using the JWT Customizer in Logto

> Customize JWT tokens in Logto by writing JavaScript to modify claims inject user data or deny access during token issuance Learn how to implement custom business logic

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: how-to-guide
- Published: 2026-07-05

---

**You can customize JWT tokens in Logto by writing a JavaScript script that runs in a sandbox during token issuance, allowing you to modify claims, inject user data, or deny access based on custom business logic.**

The **JWT Customizer** in Logto (logto-io/logto) provides a programmatic way to shape the content of issued access tokens and client credentials tokens. Instead of accepting static OIDC claims, you can inject custom data from the user context, application metadata, or external environment variables before the token is signed and returned to the client.

## Understanding the JWT Customizer Architecture

The customizer consists of a JavaScript snippet stored in the `logto_configs` table under keys like `jwt.accessToken` or `jwt.clientCredentials`. When Logto issues a token, it executes this script inside a secure sandbox with access to a controlled payload object.

### Configuration Schema and Storage

According to the source code in [`packages/schemas/src/types/logto-config/jwt-customizer.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/logto-config/jwt-customizer.ts), the customizer configuration is validated by `jwtCustomizerGuard` and related guards. The stored record contains:

- **script**: The JavaScript code string that runs during token issuance
- **environmentVariables**: Optional key-value pairs accessible via `process.env`
- **sample context**: Data shape definitions for testing

The runtime validation uses `customJwtFetcherGuard` to ensure that requests from the admin UI meet the expected schema before execution.

### Runtime Execution Flow

During token issuance, Logto performs the following steps:

1. **Load** the customizer record for the specific token type (access token or client credentials)
2. **Build** a context sample containing user, grant, interaction, application, and organization data
3. **Execute** the script in a sandbox VM with a `CustomJwtScriptPayload` object:

```typescript
{
  token: Record<string, unknown>,           // Standard OIDC claims
  context?: Record<string, unknown>,       // User, app, org data
  environmentVariables?: Record<string, string>,
  api: {
    denyAccess: (message?: string) => never // Abort token issuance
  }
}

```

4. **Merge** the returned token payload with the original claims
5. **Return** the final JWT, or an OIDC error if the script throws a `CustomJwtErrorCode`

## Accessing Context Data in Your Script

The script receives rich context data through various guards defined in the schema file. These guards ensure only serializable, non-sensitive data reaches the sandbox.

### User Context

The `jwtCustomizerUserContextGuard` provides standard user fields (`sub`, `email`) plus additional metadata:

- Password status and SSO identities
- MFA factors (without secrets)
- Assigned roles and organizations

### Grant and Interaction Context

For token exchange flows, `jwtCustomizerGrantContextGuard` contains the original subject token payload. The `jwtCustomizerUserInteractionContextGuard` captures interaction events, verification records (password, email code, MFA), and the user ID associated with the authentication event.

### Application and Organization Context

`jwtCustomizerApplicationContextGuard` exposes all application fields except secrets, while `jwtCustomizerOrganizationContextGuard` provides minimal organization data for organization-scoped tokens.

## Writing a JWT Customizer Script

Your script must export a function that accepts the payload object and optionally returns the modified token. Since the VM mutates the object in-place, you can modify `payload.token` directly.

```javascript
// Example: Add admin claim and enforce phone verification
module.exports = (payload) => {
  const { token, context, api } = payload;

  // Add custom claim based on user role
  if (context?.user?.roles?.some(r => r.name === 'admin')) {
    token.isAdmin = true;
    token.permissions = ['read', 'write', 'delete'];
  }

  // Deny access if phone not verified
  if (!context?.user?.phone || !context.user.phoneVerified) {
    api.denyAccess('Phone number verification required');
  }

  // Use environment variables
  if (process.env.CUSTOM_FLAG === 'true') {
    token.customFlag = true;
  }

  // Return optional (modification happens in-place)
  return token;
};

```

Note that you cannot import external modules; you may only use standard JavaScript features and the provided `payload` object.

## Configuring the Customizer via Admin API

To activate a customizer, POST to the configurations endpoint with a payload matching `accessTokenJwtCustomizerGuard` (lines 94-108 in the source):

```bash
curl -X POST https://localhost:3002/api/configurations \
  -H "Authorization: Bearer <ADMIN_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "jwt.accessToken",
    "value": {
      "script": "module.exports = (payload) => { payload.token.customClaim = \"value\"; };",
      "environmentVariables": {
        "CUSTOM_FLAG": "true",
        "API_VERSION": "v2"
      }
    }
  }'

```

For client credentials tokens, use the key `jwt.clientCredentials` instead.

## Testing Your Customizer

Logto provides a test endpoint at `/api/jwt/customizer/test` that validates your script against sample data without affecting live tokens. The request body conforms to `jwtCustomizerTestRequestBodyGuard` (lines 33-48):

```bash
curl -X POST https://localhost:3001/api/jwt/customizer/test \
  -H "Authorization: Bearer <ADMIN_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "tokenType": "access-token",
    "script": "module.exports = (payload) => { payload.token.foo = \"bar\"; };",
    "token": { "sub": "12345", "aud": "my-client" },
    "context": {
      "user": { 
        "id": "12345", 
        "email": "user@example.com", 
        "emailVerified": true,
        "roles": [{ "name": "admin" }]
      }
    }
  }'

```

The response contains the modified token with the new `foo` claim if execution succeeds.

## Error Handling and Access Control

You can abort token issuance by calling `api.denyAccess()` from the `CustomJwtApiContext`. This triggers an OIDC error response with code `access_denied` and a body matching `customJwtErrorBodyGuard` (lines 90-93):

```javascript
module.exports = (payload) => {
  const { token, context, api } = payload;
  
  // Custom validation logic
  if (context?.user?.roles?.length === 0) {
    api.denyAccess('User must have at least one role assigned');
  }
  
  token.authorized = true;
  return token;
};

```

When `denyAccess` is invoked, Logto immediately returns an error to the client without issuing the JWT.

## Summary

- **JWT Customizer** scripts execute in a sandbox during token issuance, allowing you to modify claims in [`packages/schemas/src/types/logto-config/jwt-customizer.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/logto-config/jwt-customizer.ts).
- The script receives a `CustomJwtScriptPayload` containing `token`, `context` (user/app/org data), `environmentVariables`, and an `api` object with `denyAccess()`.
- Context data is validated by specific guards including `jwtCustomizerUserContextGuard` and `jwtCustomizerApplicationContextGuard` to ensure security.
- Store customizers via the admin API using keys `jwt.accessToken` or `jwt.clientCredentials` with payloads matching `accessTokenJwtCustomizerGuard`.
- Test customizers safely using the `/api/jwt/customizer/test` endpoint before deploying to production.

## Frequently Asked Questions

### How do I access user roles inside the JWT customizer script?

The `context.user` object contains a `roles` array populated according to `jwtCustomizerUserContextGuard`. You can check roles using `context?.user?.roles?.some(r => r.name === 'admin')` and add conditional claims to the token payload based on the user's assigned roles.

### Can I import external npm packages in the customizer script?

No, the script runs in a restricted sandbox VM without access to external modules. You can only use standard JavaScript/TypeScript features and the data provided in the `payload` object, including `token`, `context`, and `environmentVariables` passed during configuration.

### What happens if my customizer script throws an error?

If your script throws a `CustomJwtErrorCode` or calls `api.denyAccess()`, Logto catches the error and returns an OIDC error response with code `access_denied`. The error body conforms to `customJwtErrorBodyGuard`. Other runtime errors may result in a generic token issuance failure.

### Where is the JWT customizer configuration stored?

Logto stores the customizer in the `logto_configs` database table with keys like `jwt.accessToken` for access tokens or `jwt.clientCredentials` for client credentials tokens. The record includes the script string and optional environment variables, validated by `jwtCustomizerGuard` before storage.