diff --git a/.changeset/fair-bobcats-pull.md b/.changeset/fair-bobcats-pull.md new file mode 100644 index 0000000000..aa766d6104 --- /dev/null +++ b/.changeset/fair-bobcats-pull.md @@ -0,0 +1,8 @@ +--- +'@clerk/backend': patch +'@clerk/types': patch +--- + +Add type-level validation to prevent server-side usage of system permissions + +System permissions (e.g., `org:sys_domains:manage`) are intentionally excluded from session claims to maintain reasonable JWT sizes. For more information, refer to our docs: https://clerk.com/docs/organizations/roles-permissions#system-permissions diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index e24df5830c..a0b967cb4d 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,7 +1,7 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; import type { ActClaim, - CheckAuthorizationWithCustomPermissions, + CheckAuthorizationFromSessionClaims, JwtPayload, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, @@ -42,7 +42,7 @@ export type SignedInAuthObject = { */ factorVerificationAge: [number, number] | null; getToken: ServerGetToken; - has: CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationFromSessionClaims; debug: AuthObjectDebug; }; @@ -65,7 +65,7 @@ export type SignedOutAuthObject = { */ factorVerificationAge: null; getToken: ServerGetToken; - has: CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationFromSessionClaims; debug: AuthObjectDebug; }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 9d06d9e10a..5d2429fb16 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -113,6 +113,13 @@ describe('ClerkMiddleware type tests', () => { clerkMiddlewareMock(); }); + it('prevents usage of system permissions with auth.has()', () => { + clerkMiddlewareMock(async (auth, _event, _request) => { + // @ts-expect-error - system permissions are not allowed + (await auth()).has({ permission: 'org:sys_foo' }); + }); + }); + describe('Multi domain', () => { const defaultProps = { publishableKey: '', secretKey: '' }; diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 73ef4232f4..3bd27594f9 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -97,7 +97,7 @@ export interface JwtPayload extends CustomJwtSessionClaims { org_role?: OrganizationCustomRoleKey; /** - * Active organization role + * Active organization permissions */ org_permissions?: OrganizationCustomPermissionKey[]; diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 8a010d1991..93cf0be204 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -68,13 +68,14 @@ export type OrganizationCustomRoleKey = ClerkAuthorization extends Placeholder : Base['role'] : Base['role']; +export type OrganizationSystemPermissionPrefix = 'org:sys_'; export type OrganizationSystemPermissionKey = - | 'org:sys_domains:manage' - | 'org:sys_profile:manage' - | 'org:sys_profile:delete' - | 'org:sys_memberships:read' - | 'org:sys_memberships:manage' - | 'org:sys_domains:read'; + | `${OrganizationSystemPermissionPrefix}domains:manage` + | `${OrganizationSystemPermissionPrefix}profile:manage` + | `${OrganizationSystemPermissionPrefix}profile:delete` + | `${OrganizationSystemPermissionPrefix}memberships:read` + | `${OrganizationSystemPermissionPrefix}memberships:manage` + | `${OrganizationSystemPermissionPrefix}domains:read`; /** * OrganizationPermissionKey is a combination of system and custom permissions. diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 028ceacee3..f62bf0d317 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -15,6 +15,7 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationPermissionKey, + OrganizationSystemPermissionPrefix, } from './organizationMembership'; import type { ClerkResource } from './resource'; import type { @@ -25,6 +26,29 @@ import type { import type { TokenResource } from './token'; import type { UserResource } from './user'; +type DisallowSystemPermissions

= P extends `${OrganizationSystemPermissionPrefix}${string}` + ? 'System permissions are not included in session claims and cannot be used on the server-side' + : P; + +/** + * Type guard for server-side authorization checks using session claims. + * System permissions are not allowed since they are not included + * in session claims and cannot be verified on the server side. + */ +export type CheckAuthorizationFromSessionClaims =

( + isAuthorizedParams: WithReverification< + | { + role: OrganizationCustomRoleKey; + permission?: never; + } + | { + role?: never; + permission: DisallowSystemPermissions

; + } + | { role?: never; permission?: never } + >, +) => boolean; + export type CheckAuthorizationFn = (isAuthorizedParams: Params) => boolean; export type CheckAuthorizationWithCustomPermissions =