diff --git a/packages/console/src/components/ItemPreview/UserPreview.tsx b/packages/console/src/components/ItemPreview/UserPreview.tsx
index 7f16a3d49..f2aaf3785 100644
--- a/packages/console/src/components/ItemPreview/UserPreview.tsx
+++ b/packages/console/src/components/ItemPreview/UserPreview.tsx
@@ -1,4 +1,4 @@
-import { type User } from '@logto/schemas';
+import { type UserInfo } from '@logto/schemas';
 import { conditional } from '@silverhand/essentials';
 
 import SuspendedTag from '@/pages/Users/components/SuspendedTag';
@@ -9,7 +9,7 @@ import UserAvatar from '../UserAvatar';
 import ItemPreview from '.';
 
 type Props = {
-  user: User;
+  user: UserInfo;
 };
 
 /** A component that renders a preview of a user. It's useful for displaying a user in a list. */
diff --git a/packages/console/src/components/UserAccountInformation/index.tsx b/packages/console/src/components/UserAccountInformation/index.tsx
index ad4fc6676..e1260b998 100644
--- a/packages/console/src/components/UserAccountInformation/index.tsx
+++ b/packages/console/src/components/UserAccountInformation/index.tsx
@@ -1,5 +1,5 @@
 import type { AdminConsoleKey } from '@logto/phrases';
-import type { User } from '@logto/schemas';
+import type { UserProfileResponse } from '@logto/schemas';
 import { conditionalArray } from '@silverhand/essentials';
 import { useState } from 'react';
 import { toast } from 'react-hot-toast';
@@ -15,7 +15,7 @@ import * as modalStyles from '@/scss/modal.module.scss';
 import * as styles from './index.module.scss';
 
 type Props = {
-  user: User;
+  user: UserProfileResponse;
   password: string;
   title: AdminConsoleKey;
   onClose: () => void;
diff --git a/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx b/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
index e7b93fb97..9d6197c06 100644
--- a/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
+++ b/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
@@ -1,4 +1,4 @@
-import type { RoleResponse, User, Application } from '@logto/schemas';
+import type { RoleResponse, UserProfileResponse, Application } from '@logto/schemas';
 import { RoleType } from '@logto/schemas';
 import { useState } from 'react';
 import { toast } from 'react-hot-toast';
@@ -15,7 +15,7 @@ import { getUserTitle } from '@/utils/user';
 
 type Props =
   | {
-      entity: User;
+      entity: UserProfileResponse;
       onClose: (success?: boolean) => void;
       type: RoleType.User;
     }
diff --git a/packages/console/src/pages/UserDetails/utils.ts b/packages/console/src/pages/UserDetails/utils.ts
index 053d6f8d5..f6e32b82f 100644
--- a/packages/console/src/pages/UserDetails/utils.ts
+++ b/packages/console/src/pages/UserDetails/utils.ts
@@ -1,11 +1,11 @@
-import type { User } from '@logto/schemas';
+import type { UserProfileResponse } from '@logto/schemas';
 import { formatToInternationalPhoneNumber } from '@logto/shared/universal';
 import { conditional } from '@silverhand/essentials';
 
 import type { UserDetailsForm } from './types';
 
 export const userDetailsParser = {
-  toLocalForm: (data: User): UserDetailsForm => {
+  toLocalForm: (data: UserProfileResponse): UserDetailsForm => {
     const { primaryEmail, primaryPhone, username, name, avatar, customData } = data;
     const parsedPhoneNumber = conditional(
       primaryPhone && formatToInternationalPhoneNumber(primaryPhone)
diff --git a/packages/console/src/utils/user.ts b/packages/console/src/utils/user.ts
index 6e81cc191..03cf80907 100644
--- a/packages/console/src/utils/user.ts
+++ b/packages/console/src/utils/user.ts
@@ -1,11 +1,11 @@
-import type { User } from '@logto/schemas';
+import type { UserInfo } from '@logto/schemas';
 import { getUserDisplayName } from '@logto/shared/universal';
 import { t } from 'i18next';
 
-export const getUserTitle = (user?: User): string =>
+export const getUserTitle = (user?: UserInfo): string =>
   (user ? getUserDisplayName(user) : undefined) ?? t('admin_console.users.unnamed');
 
-export const getUserSubtitle = (user?: User) => {
+export const getUserSubtitle = (user?: UserInfo) => {
   if (!user?.name) {
     return;
   }
diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index 2fba78875..2e1d73e7b 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -1,77 +1,64 @@
 import type { JwtCustomizerUserContext } from '@logto/schemas';
-import {
-  userInfoSelectFields,
-  OrganizationScopes,
-  jwtCustomizerUserContextGuard,
-} from '@logto/schemas';
+import { userInfoSelectFields, jwtCustomizerUserContextGuard } from '@logto/schemas';
 import { deduplicate, pick, pickState } from '@silverhand/essentials';
 
+import { type ScopeLibrary } from '#src/libraries/scope.js';
 import { type UserLibrary } from '#src/libraries/user.js';
 import type Queries from '#src/tenants/Queries.js';
 
-export const createJwtCustomizerLibrary = (queries: Queries, userLibrary: UserLibrary) => {
+export const createJwtCustomizerLibrary = (
+  queries: Queries,
+  userLibrary: UserLibrary,
+  scopeLibrary: ScopeLibrary
+) => {
   const {
     users: { findUserById },
-    rolesScopes: { findRolesScopesByRoleId },
-    scopes: { findScopeById },
-    resources: { findResourceById },
+    rolesScopes: { findRolesScopesByRoleIds },
+    scopes: { findScopesByIds },
     userSsoIdentities,
     organizations: { relations },
   } = queries;
   const { findUserRoles } = userLibrary;
+  const { attachResourceToScopes } = scopeLibrary;
 
+  /**
+   * We does not include org roles' scopes for the following reason:
+   * 1. The org scopes query method requires `limit` and `offset` parameters. Other management API get
+   * these APIs from console setup while this library method is a backend used method.
+   * 2. Logto developers can get the org roles' id from this user context and hence query the org roles' scopes via management API.
+   */
   const getUserContext = async (userId: string): Promise<JwtCustomizerUserContext> => {
     const user = await findUserById(userId);
     const fullSsoIdentities = await userSsoIdentities.findUserSsoIdentitiesByUserId(userId);
     const roles = await findUserRoles(userId);
+    const rolesScopes = await findRolesScopesByRoleIds(roles.map(({ id }) => id));
+    const scopeIds = rolesScopes.map(({ scopeId }) => scopeId);
+    const scopes = await findScopesByIds(scopeIds);
+    const scopesWithResources = await attachResourceToScopes(scopes);
     const organizationsWithRoles = await relations.users.getOrganizationsByUserId(userId);
     const userContext = {
       ...pick(user, ...userInfoSelectFields),
       ssoIdentities: fullSsoIdentities.map(pickState('issuer', 'identityId', 'detail')),
       mfaVerificationFactors: deduplicate(user.mfaVerifications.map(({ type }) => type)),
-      roles: await Promise.all(
-        roles.map(async (role) => {
-          const fullRolesScopes = await findRolesScopesByRoleId(role.id);
-          const scopeIds = fullRolesScopes.map(({ scopeId }) => scopeId);
-          return {
-            ...pick(role, 'id', 'name', 'description'),
-            scopes: await Promise.all(
-              scopeIds.map(async (scopeId) => {
-                const scope = await findScopeById(scopeId);
-                return {
-                  ...pick(scope, 'id', 'name', 'description'),
-                  ...(await findResourceById(scope.resourceId).then(
-                    ({ indicator, id: resourceId }) => ({ indicator, resourceId })
-                  )),
-                };
-              })
-            ),
-          };
-        })
-      ),
-      // No need to deal with the type here, the type will be enforced by the guard when return the result.
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-      organizations: Object.fromEntries(
-        await Promise.all(
-          organizationsWithRoles.map(async ({ organizationRoles, ...organization }) => [
-            organization.id,
-            {
-              roles: await Promise.all(
-                organizationRoles.map(async ({ id, name }) => {
-                  const [_, fullOrganizationScopes] = await relations.rolesScopes.getEntities(
-                    OrganizationScopes,
-                    { organizationRoleId: id }
-                  );
-                  return {
-                    id,
-                    name,
-                    scopes: fullOrganizationScopes.map(pickState('id', 'name', 'description')),
-                  };
-                })
-              ),
-            },
-          ])
-        )
+      roles: roles.map((role) => {
+        const scopeIds = new Set(
+          rolesScopes.filter(({ roleId }) => roleId === role.id).map(({ scopeId }) => scopeId)
+        );
+        return {
+          ...pick(role, 'id', 'name', 'description'),
+          scopes: scopesWithResources
+            .filter(({ id }) => scopeIds.has(id))
+            .map(pickState('id', 'name', 'description', 'resourceId', 'resource')),
+        };
+      }),
+      organizations: organizationsWithRoles.map(pickState('id', 'name', 'description')),
+      organizationRoles: organizationsWithRoles.flatMap(
+        ({ id: organizationId, organizationRoles }) =>
+          organizationRoles.map(({ id: roleId, name: roleName }) => ({
+            organizationId,
+            roleId,
+            roleName,
+          }))
       ),
     };
 
diff --git a/packages/core/src/libraries/scope.ts b/packages/core/src/libraries/scope.ts
new file mode 100644
index 000000000..2b6d60fed
--- /dev/null
+++ b/packages/core/src/libraries/scope.ts
@@ -0,0 +1,30 @@
+import type { Scope, ScopeResponse } from '@logto/schemas';
+
+import type Queries from '#src/tenants/Queries.js';
+import assertThat from '#src/utils/assert-that.js';
+
+export type ScopeLibrary = ReturnType<typeof createScopeLibrary>;
+
+export const createScopeLibrary = (queries: Queries) => {
+  const {
+    resources: { findResourcesByIds },
+  } = queries;
+
+  const attachResourceToScopes = async (scopes: readonly Scope[]): Promise<ScopeResponse[]> => {
+    const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId));
+    return scopes.map((scope) => {
+      const resource = resources.find(({ id }) => id === scope.resourceId);
+
+      assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`));
+
+      return {
+        ...scope,
+        resource,
+      };
+    });
+  };
+
+  return {
+    attachResourceToScopes,
+  };
+};
diff --git a/packages/core/src/routes/role.scope.ts b/packages/core/src/routes/role.scope.ts
index 8cbafdf39..61b731cb6 100644
--- a/packages/core/src/routes/role.scope.ts
+++ b/packages/core/src/routes/role.scope.ts
@@ -1,4 +1,3 @@
-import type { Scope, ScopeResponse } from '@logto/schemas';
 import { scopeResponseGuard, Scopes } from '@logto/schemas';
 import { generateStandardId } from '@logto/shared';
 import { tryThat } from '@silverhand/essentials';
@@ -7,7 +6,6 @@ import { object, string } from 'zod';
 import RequestError from '#src/errors/RequestError/index.js';
 import koaGuard from '#src/middleware/koa-guard.js';
 import koaPagination from '#src/middleware/koa-pagination.js';
-import assertThat from '#src/utils/assert-that.js';
 import { parseSearchParamsForSearch } from '#src/utils/search.js';
 
 import type { AuthedRouter, RouterInitArgs } from './types.js';
@@ -16,7 +14,6 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
   ...[router, { queries, libraries }]: RouterInitArgs<T>
 ) {
   const {
-    resources: { findResourcesByIds },
     rolesScopes: { deleteRolesScope, findRolesScopesByRoleId, insertRolesScopes },
     roles: { findRoleById },
     scopes: { findScopesByIds, countScopesByScopeIds, searchScopesByScopeIds },
@@ -24,22 +21,9 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
   const {
     quota,
     roleScopes: { validateRoleScopeAssignment },
+    scopes: { attachResourceToScopes },
   } = libraries;
 
-  const attachResourceToScopes = async (scopes: readonly Scope[]): Promise<ScopeResponse[]> => {
-    const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId));
-    return scopes.map((scope) => {
-      const resource = resources.find(({ id }) => id === scope.resourceId);
-
-      assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`));
-
-      return {
-        ...scope,
-        resource,
-      };
-    });
-  };
-
   router.get(
     '/roles/:id/scopes',
     koaPagination({ isOptional: true }),
diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts
index 449c92553..12a703748 100644
--- a/packages/core/src/tenants/Libraries.ts
+++ b/packages/core/src/tenants/Libraries.ts
@@ -10,6 +10,7 @@ import { createPhraseLibrary } from '#src/libraries/phrase.js';
 import { createProtectedAppLibrary } from '#src/libraries/protected-app.js';
 import { createQuotaLibrary } from '#src/libraries/quota.js';
 import { createRoleScopeLibrary } from '#src/libraries/role-scope.js';
+import { createScopeLibrary } from '#src/libraries/scope.js';
 import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
 import { createSocialLibrary } from '#src/libraries/social.js';
 import { createSsoConnectorLibrary } from '#src/libraries/sso-connector.js';
@@ -22,8 +23,9 @@ export default class Libraries {
   users = createUserLibrary(this.queries);
   phrases = createPhraseLibrary(this.queries);
   hooks = createHookLibrary(this.queries);
+  scopes = createScopeLibrary(this.queries);
   socials = createSocialLibrary(this.queries, this.connectors);
-  jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users);
+  jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users, this.scopes);
   passcodes = createPasscodeLibrary(this.queries, this.connectors);
   applications = createApplicationLibrary(this.queries);
   verificationStatuses = createVerificationStatusLibrary(this.queries);
diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts
index ca4b5c551..22a21a712 100644
--- a/packages/schemas/src/types/jwt-customizer.ts
+++ b/packages/schemas/src/types/jwt-customizer.ts
@@ -1,43 +1,33 @@
 import { z } from 'zod';
 
-import {
-  OrganizationRoles,
-  OrganizationScopes,
-  Resources,
-  Roles,
-  Scopes,
-  UserSsoIdentities,
-} from '../db-entries/index.js';
+import { Organizations, Roles, UserSsoIdentities } from '../db-entries/index.js';
 import { mfaFactorsGuard, jsonObjectGuard } from '../foundations/index.js';
 
 import { jwtCustomizerGuard } from './logto-config/index.js';
+import { scopeResponseGuard } from './scope.js';
 import { userInfoGuard } from './user.js';
 
-const organizationDetailGuard = z.object({
-  roles: z.array(
-    OrganizationRoles.guard.pick({ id: true, name: true }).extend({
-      scopes: z.array(OrganizationScopes.guard.pick({ id: true, name: true, description: true })),
-    })
-  ),
-});
-
-export type OrganizationDetail = z.infer<typeof organizationDetailGuard>;
-
 export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
-  ssoIdentities: z.array(
-    UserSsoIdentities.guard.pick({ issuer: true, identityId: true, detail: true })
-  ),
+  ssoIdentities: UserSsoIdentities.guard
+    .pick({ issuer: true, identityId: true, detail: true })
+    .array(),
   mfaVerificationFactors: mfaFactorsGuard,
-  roles: z.array(
-    Roles.guard.pick({ id: true, name: true, description: true }).extend({
-      scopes: z.array(
-        Scopes.guard
-          .pick({ id: true, name: true, description: true, resourceId: true })
-          .merge(Resources.guard.pick({ indicator: true }))
-      ),
+  roles: Roles.guard
+    .pick({ id: true, name: true, description: true })
+    .extend({
+      scopes: scopeResponseGuard
+        .pick({ id: true, name: true, description: true, resourceId: true, resource: true })
+        .array(),
     })
-  ),
-  organizations: z.record(organizationDetailGuard),
+    .array(),
+  organizations: Organizations.guard.pick({ id: true, name: true, description: true }).array(),
+  organizationRoles: z
+    .object({
+      organizationId: z.string(),
+      roleId: z.string(),
+      roleName: z.string(),
+    })
+    .array(),
 });
 
 export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
diff --git a/packages/schemas/src/types/user.ts b/packages/schemas/src/types/user.ts
index d51d16203..a34c763ce 100644
--- a/packages/schemas/src/types/user.ts
+++ b/packages/schemas/src/types/user.ts
@@ -19,7 +19,11 @@ export const userInfoSelectFields = Object.freeze([
 ] as const);
 
 export const userInfoGuard = Users.guard.pick(
-  Object.fromEntries(userInfoSelectFields.map((key) => [key, true]))
+  // eslint-disable-next-line no-restricted-syntax
+  Object.fromEntries(userInfoSelectFields.map((field) => [field, true])) as Record<
+    (typeof userInfoSelectFields)[number],
+    true
+  >
 );
 
 export type UserInfo = z.infer<typeof userInfoGuard>;