Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/forty-trains-fail.md
Original file line number Diff line number Diff line change
@@ -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
```
55 changes: 55 additions & 0 deletions packages/backend/src/tokens/__tests__/authObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JwtPayload>;

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', () => {
Expand Down Expand Up @@ -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<JwtPayload>;

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;

Expand Down
6 changes: 3 additions & 3 deletions packages/shared/src/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
};
Expand Down Expand Up @@ -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 = ({
Expand Down
Loading