diff --git a/.changeset/forty-trains-fail.md b/.changeset/forty-trains-fail.md new file mode 100644 index 00000000000..f515944f6e3 --- /dev/null +++ b/.changeset/forty-trains-fail.md @@ -0,0 +1,20 @@ +--- +'@clerk/shared': patch +--- + +Bug fix: In `createCheckAuthorization` allow for old `org_role` format in JWT v1 where `org:` is missing. + +Example session claims: +```json +{ + "org_id": "org_xxxx", + "org_permissions": [], + "org_role": "admin", + "org_slug": "test" +} +``` +Code +```ts +authObject.has({ role: 'org:admin' }) // -> true +authObject.has({ role: 'admin' }) // -> true +``` diff --git a/packages/backend/src/tokens/__tests__/authObjects.test.ts b/packages/backend/src/tokens/__tests__/authObjects.test.ts index 02746b3f143..f79f9b17ddb 100644 --- a/packages/backend/src/tokens/__tests__/authObjects.test.ts +++ b/packages/backend/src/tokens/__tests__/authObjects.test.ts @@ -81,6 +81,34 @@ describe('signedInAuthObject', () => { expect(authObject.has({ feature: 'org:reservations' })).toBe(false); expect(authObject.has({ feature: 'org:impersonation' })).toBe(false); }); + + it('has() for orgs for old `admin` role', () => { + const mockAuthenticateContext = { sessionToken: 'authContextToken' } as AuthenticateContext; + + const partialJwtPayload = { + ___raw: 'raw', + act: { sub: 'actor' }, + sid: 'sessionId', + org_id: 'orgId', + org_role: 'admin', + org_slug: 'orgSlug', + org_permissions: ['org:f1:read', 'org:f2:manage'], + sub: 'userId', + } as Partial; + + const authObject = signedInAuthObject(mockAuthenticateContext, 'token', partialJwtPayload as JwtPayload); + + expect(authObject.has({ role: 'org:admin' })).toBe(true); + expect(authObject.has({ role: 'admin' })).toBe(true); + expect(authObject.has({ permission: 'org:f1:read' })).toBe(true); + expect(authObject.has({ permission: 'f1:read' })).toBe(true); + expect(authObject.has({ permission: 'org:f1' })).toBe(false); + expect(authObject.has({ permission: 'org:f2:manage' })).toBe(true); + expect(authObject.has({ permission: 'org:f2' })).toBe(false); + + expect(authObject.has({ feature: 'org:reservations' })).toBe(false); + expect(authObject.has({ feature: 'org:impersonation' })).toBe(false); + }); }); describe('JWT v2', () => { @@ -120,6 +148,33 @@ describe('signedInAuthObject', () => { expect(authObject.has({ feature: 'org:impersonation' })).toBe(true); }); + // This state should not happen since the JWT v2 payload is normalized to remove the `org:` prefix from o.rol. + it('has() for orgs with `org:` prefix in role', () => { + const mockAuthenticateContext = { sessionToken: 'authContextToken' } as AuthenticateContext; + + const partialJwtPayload = { + v: 2, + ___raw: 'raw', + act: { sub: 'actor' }, + sid: 'sessionId', + fea: 'o:reservations,o:impersonation', + o: { + id: 'orgId', + rol: 'org:admin', + slg: 'orgSlug', + per: 'read,manage', + fpm: '3', + }, + + sub: 'userId', + } as Partial; + + const authObject = signedInAuthObject(mockAuthenticateContext, 'token', partialJwtPayload as JwtPayload); + + expect(authObject.has({ role: 'org:admin' })).toBe(true); + expect(authObject.has({ role: 'admin' })).toBe(true); + }); + it('has() for billing with scopes', () => { const mockAuthenticateContext = { sessionToken: 'authContextToken' } as AuthenticateContext; diff --git a/packages/shared/src/authorization.ts b/packages/shared/src/authorization.ts index e8f3bee080e..cb8c7e04428 100644 --- a/packages/shared/src/authorization.ts +++ b/packages/shared/src/authorization.ts @@ -70,7 +70,7 @@ const isValidMaxAge = (maxAge: any) => typeof maxAge === 'number' && maxAge > 0; const isValidLevel = (level: any) => ALLOWED_LEVELS.has(level); const isValidVerificationType = (type: any) => ALLOWED_TYPES.has(type); -const prefixWithOrg = (value: string) => (value.startsWith('org:') ? value : `org:${value}`); +const prefixWithOrg = (value: string) => value.replace(/^(org:)*/, 'org:'); /** * Checks if a user has the required organization-level authorization. @@ -92,7 +92,7 @@ const checkOrgAuthorization: CheckOrgAuthorization = (params, options) => { } if (params.role) { - return orgRole === prefixWithOrg(params.role); + return prefixWithOrg(orgRole) === prefixWithOrg(params.role); } return null; }; @@ -236,7 +236,7 @@ type AuthStateOptions = { /** * Shared utility function that centralizes auth state resolution logic, - * preventing duplication across different packages + * preventing duplication across different packages. * @internal */ const resolveAuthState = ({