From 54797cd41716bff50424d6a5d3faae86d32a9fcc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:32:05 -0300 Subject: [PATCH 1/7] Fix JSDocs for `org_permissions` --- .../nextjs/src/server/__tests__/clerkMiddleware.test.ts | 7 +++++++ packages/types/src/jwtv2.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 9d06d9e10a5..5d2429fb16b 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 73ef4232f4d..3bd27594f9f 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[]; From d07fb963a85adb0cd7bacd0eae10603b89faed41 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:32:42 -0300 Subject: [PATCH 2/7] Extract system permission prefix to a separate type --- packages/types/src/organizationMembership.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 8a010d19911..3fad5b8a701 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -68,13 +68,15 @@ 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. From c3e62351df87dfdb4db5409613e83c82b9e01514 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:43:34 -0300 Subject: [PATCH 3/7] Add type guard from session claims --- packages/backend/src/tokens/authObjects.ts | 6 ++--- packages/types/src/organizationMembership.ts | 1 - packages/types/src/session.ts | 24 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index e24df5830c6..a0b967cb4df 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/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 3fad5b8a701..93cf0be204c 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -69,7 +69,6 @@ export type OrganizationCustomRoleKey = ClerkAuthorization extends Placeholder : Base['role']; export type OrganizationSystemPermissionPrefix = 'org:sys_'; - export type OrganizationSystemPermissionKey = | `${OrganizationSystemPermissionPrefix}domains:manage` | `${OrganizationSystemPermissionPrefix}profile:manage` diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 028ceacee31..f62bf0d317e 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 = From 2f87d65ce356ddaf08d80110f39232c657d5d1eb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:59:20 -0300 Subject: [PATCH 4/7] Add changeset --- .changeset/fair-bobcats-pull.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/fair-bobcats-pull.md diff --git a/.changeset/fair-bobcats-pull.md b/.changeset/fair-bobcats-pull.md new file mode 100644 index 00000000000..aa766d61040 --- /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 From 1da1b64b4db21806401d5d338a746d290acb725c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:22:29 -0300 Subject: [PATCH 5/7] Add type test for `useAuth.has` --- packages/react/src/hooks/__tests__/useAuth.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react/src/hooks/__tests__/useAuth.test.tsx b/packages/react/src/hooks/__tests__/useAuth.test.tsx index c4272cff3b4..8576e90f9d7 100644 --- a/packages/react/src/hooks/__tests__/useAuth.test.tsx +++ b/packages/react/src/hooks/__tests__/useAuth.test.tsx @@ -188,4 +188,12 @@ describe('useDerivedAuth', () => { expect(current.has?.({ permission: 'test' })).toBe('mocked-result'); expect(mockHas).toHaveBeenCalledWith({ permission: 'test' }); }); + + it('allows to pass system permissions', () => { + const { + result: { current }, + } = renderHook(() => useDerivedAuth({ sessionId: null, userId: null })); + + current.has?.({ permission: 'org:sys_foo' }); + }); }); From 2db90d3bf776a786c5d3ed8f2b50822f7374e236 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 7 Jan 2025 15:31:01 +0200 Subject: [PATCH 6/7] chore(types,nextjs): Handle types in `protect()` (#4843) --- packages/nextjs/src/app-router/server/auth.ts | 3 +- .../server/__tests__/clerkMiddleware.test.ts | 4 ++ packages/nextjs/src/server/protect.ts | 10 ++++- packages/types/src/session.ts | 40 ++++++++++--------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index e8e912cf435..8079e6a8bc1 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -52,7 +52,7 @@ export const auth: AuthFn = async () => { return Object.assign(authObject, { redirectToSignIn }); }; -auth.protect = async (...args) => { +auth.protect = async (...args: any[]) => { require('server-only'); const request = await buildRequestLike(); @@ -66,6 +66,5 @@ auth.protect = async (...args) => { redirect, }); - // @ts-expect-error TS flattens all possible combinations of the for AuthProtect signatures in a union. return protect(...args); }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 5d2429fb16b..99bbacd6bee 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -117,6 +117,10 @@ describe('ClerkMiddleware type tests', () => { clerkMiddlewareMock(async (auth, _event, _request) => { // @ts-expect-error - system permissions are not allowed (await auth()).has({ permission: 'org:sys_foo' }); + // @ts-expect-error - system permissions are not allowed + await auth.protect(has => has({ permission: 'org:sys_foo' })); + // @ts-expect-error - system permissions are not allowed + await auth.protect({ permission: 'org:sys_foo' }); }); }); diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 7e17e37817c..3c683b42216 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -2,8 +2,11 @@ import type { AuthObject } from '@clerk/backend'; import type { RedirectFun, SignedInAuthObject } from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; import type { + CheckAuthorizationFromSessionClaims, + CheckAuthorizationParamsFromSessionClaims, CheckAuthorizationParamsWithCustomPermissions, CheckAuthorizationWithCustomPermissions, + OrganizationCustomPermissionKey, } from '@clerk/types'; import { constants as nextConstants } from '../constants'; @@ -15,10 +18,13 @@ type AuthProtectOptions = { unauthorizedUrl?: string; unauthenticatedUrl?: strin * Throws a Nextjs notFound error if user is not authenticated or authorized. */ export interface AuthProtect { - (params?: CheckAuthorizationParamsWithCustomPermissions, options?: AuthProtectOptions): Promise; +

( + params?: CheckAuthorizationParamsFromSessionClaims

, + options?: AuthProtectOptions, + ): Promise; ( - params?: (has: CheckAuthorizationWithCustomPermissions) => boolean, + params?: (has: CheckAuthorizationFromSessionClaims) => boolean, options?: AuthProtectOptions, ): Promise; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index f62bf0d317e..65bc988ab56 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -30,25 +30,6 @@ type DisallowSystemPermissions

= P extends `${OrganizationSyst ? '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 = @@ -87,6 +68,27 @@ type CheckAuthorizationParams = WithReverification< } >; +/** + * 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: CheckAuthorizationParamsFromSessionClaims

, +) => boolean; + +export type CheckAuthorizationParamsFromSessionClaims

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

; + } + | { role?: never; permission?: never } +>; + export interface SessionResource extends ClerkResource { id: string; status: SessionStatus; From afad6c9fa315eed4909458495df7ecd9961cc675 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:33:46 -0300 Subject: [PATCH 7/7] Include `@clerk/nextjs` on changeset --- .changeset/fair-bobcats-pull.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/fair-bobcats-pull.md b/.changeset/fair-bobcats-pull.md index aa766d61040..8ae6aac38d3 100644 --- a/.changeset/fair-bobcats-pull.md +++ b/.changeset/fair-bobcats-pull.md @@ -1,6 +1,7 @@ --- '@clerk/backend': patch '@clerk/types': patch +'@clerk/nextjs': patch --- Add type-level validation to prevent server-side usage of system permissions