Skip to content

Commit 113dceb

Browse files
committed
refactor(clerk-js,shared,type): Support new claims and keep orgPermissions on authObject backwards compatible
1 parent 7fd491f commit 113dceb

File tree

8 files changed

+290
-159
lines changed

8 files changed

+290
-159
lines changed

packages/backend/src/tokens/authObjects.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import {
2-
__experimental_resolveSignedInAuthStateFromJWTClaims,
3-
createCheckAuthorization,
4-
} from '@clerk/shared/authorization';
1+
import { createCheckAuthorization } from '@clerk/shared/authorization';
2+
import { __experimental_JWTPayloadToAuthObjectProperties } from '@clerk/shared/jwtPayloadParser';
53
import type {
64
ActClaim,
75
CheckAuthorizationFromSessionClaims,
@@ -104,7 +102,7 @@ export function signedInAuthObject(
104102
sessionClaims: JwtPayload,
105103
): SignedInAuthObject {
106104
const { actor, sessionId, sessionStatus, userId, orgId, orgRole, orgSlug, orgPermissions, factorVerificationAge } =
107-
__experimental_resolveSignedInAuthStateFromJWTClaims(sessionClaims);
105+
__experimental_JWTPayloadToAuthObjectProperties(sessionClaims);
108106
const apiClient = createBackendApiClient(authenticateContext);
109107
const getToken = createGetToken({
110108
sessionId,

packages/clerk-js/src/core/jwt-client.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { __experimental_resolveSignedInAuthStateFromJWTClaims } from '@clerk/shared/authorization';
1+
import { __experimental_JWTPayloadToAuthObjectProperties } from '@clerk/shared/jwtPayloadParser';
22
import type {
33
ClientJSON,
44
OrganizationMembershipJSON,
@@ -45,7 +45,7 @@ export function createClientFromJwt(jwt: string | undefined | null): Client {
4545
}
4646

4747
const { sessionId, userId, orgId, orgRole, orgPermissions, orgSlug, factorVerificationAge } =
48-
__experimental_resolveSignedInAuthStateFromJWTClaims(token.jwt.claims);
48+
__experimental_JWTPayloadToAuthObjectProperties(token.jwt.claims);
4949

5050
// TODO(jwt-v2): when JWT version 2 is available, we should revise org permissions
5151
const defaultClient = {

packages/shared/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@
117117
"web3",
118118
"getEnvVariable",
119119
"pathMatcher",
120-
"organization"
120+
"organization",
121+
"jwtPayloadParser"
121122
],
122123
"scripts": {
123124
"build": "tsup",

packages/shared/src/__tests__/authorization.test.ts

-74
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { __experimental_JWTPayloadToAuthObjectProperties as JWTPayloadToAuthObjectProperties } from '../jwtPayloadParser';
2+
3+
const baseClaims = {
4+
exp: 1234567890,
5+
iat: 1234567890,
6+
iss: 'https://api.clerk.com',
7+
sub: 'sub',
8+
sid: 'sid',
9+
azp: 'azp',
10+
nbf: 1234567890,
11+
__raw: '',
12+
};
13+
14+
describe('resolveSignedInAuthStateFromJWTClaims', () => {
15+
test.skip('produced auth object is the same for v1 and v2', () => {
16+
const { sessionClaims: v2Claims, ...signedInAuthObjectV2 } = JWTPayloadToAuthObjectProperties({
17+
...baseClaims,
18+
v: 2,
19+
o: {
20+
id: 'org_id',
21+
rol: 'admin',
22+
slg: 'org_slug',
23+
per: 'permission1,permission2',
24+
},
25+
});
26+
27+
const { sessionClaims: v1Claims, ...signedInAuthObjectV1 } = JWTPayloadToAuthObjectProperties({
28+
...baseClaims,
29+
org_id: 'org_id',
30+
org_role: 'admin',
31+
org_slug: 'org_slug',
32+
org_permissions: ['org:permission1', 'permission2'],
33+
});
34+
expect(signedInAuthObjectV1).toEqual(signedInAuthObjectV2);
35+
});
36+
37+
test('org permissions are generated correctly when fea, per, and fpm are present', () => {
38+
const { sessionClaims: v2Claims, ...signedInAuthObject } = JWTPayloadToAuthObjectProperties({
39+
...baseClaims,
40+
v: 2,
41+
fea: 'o:impersonation,o:memberships',
42+
o: {
43+
id: 'org_id',
44+
rol: 'admin',
45+
slg: 'org_slug',
46+
per: 'read,manage',
47+
fpm: '2,3',
48+
},
49+
});
50+
51+
expect(signedInAuthObject.orgPermissions?.sort()).toEqual(
52+
['org:impersonation:read', 'org:memberships:read', 'org:memberships:manage'].sort(),
53+
);
54+
});
55+
56+
test('if a feature is not mapped to any permissions it is added as is to the orgPermissions array', () => {
57+
const { sessionClaims: v2Claims, ...signedInAuthObject } = JWTPayloadToAuthObjectProperties({
58+
...baseClaims,
59+
v: 2,
60+
fea: 'o:impersonation,o:memberships,o:feature3',
61+
o: {
62+
id: 'org_id',
63+
rol: 'admin',
64+
slg: 'org_slug',
65+
per: 'read,manage',
66+
fpm: '2,3',
67+
},
68+
});
69+
70+
expect(signedInAuthObject.orgPermissions?.sort()).toEqual(
71+
['org:impersonation:read', 'org:memberships:read', 'org:memberships:manage'].sort(),
72+
);
73+
});
74+
75+
test('includes both org and user scoped features', () => {
76+
const { sessionClaims: v2Claims, ...signedInAuthObject } = JWTPayloadToAuthObjectProperties({
77+
...baseClaims,
78+
v: 2,
79+
fea: 'uo:impersonation,o:memberships,uo:feature3',
80+
o: {
81+
id: 'org_id',
82+
rol: 'admin',
83+
slg: 'org_slug',
84+
per: 'read,manage',
85+
fpm: '2,3,2',
86+
},
87+
});
88+
89+
expect(signedInAuthObject.orgPermissions?.sort()).toEqual(
90+
['org:impersonation:read', 'org:memberships:read', 'org:memberships:manage', 'org:feature3:read'].sort(),
91+
);
92+
});
93+
94+
test('feature are user scoped only', () => {
95+
const { sessionClaims: v2Claims, ...signedInAuthObject } = JWTPayloadToAuthObjectProperties({
96+
...baseClaims,
97+
v: 2,
98+
fea: 'u:impersonation,u:memberships,u:feature3',
99+
o: {
100+
id: 'org_id',
101+
rol: 'admin',
102+
slg: 'org_slug',
103+
},
104+
});
105+
106+
expect(signedInAuthObject.orgPermissions?.sort()).toEqual([]);
107+
});
108+
});

packages/shared/src/authorization.ts

+2-75
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type {
22
ActClaim,
33
CheckAuthorizationWithCustomPermissions,
44
GetToken,
5-
JwtPayload,
65
OrganizationCustomPermissionKey,
76
OrganizationCustomRoleKey,
87
PendingSessionOptions,
@@ -13,6 +12,7 @@ import type {
1312
SignOut,
1413
UseAuthReturn,
1514
} from '@clerk/types';
15+
import { permission } from 'process';
1616

1717
type TypesToConfig = Record<SessionVerificationTypes, Exclude<ReverificationConfig, SessionVerificationTypes>>;
1818
type AuthorizationOptions = {
@@ -273,77 +273,4 @@ const resolveAuthState = ({
273273
}
274274
};
275275

276-
/**
277-
* @internal
278-
*/
279-
type SignedInAuthObjectProperties = {
280-
sessionClaims: JwtPayload;
281-
sessionId: string;
282-
sessionStatus: SessionStatusClaim | null;
283-
actor: ActClaim | undefined;
284-
userId: string;
285-
orgId: string | undefined;
286-
orgRole: OrganizationCustomRoleKey | undefined;
287-
orgSlug: string | undefined;
288-
orgPermissions: OrganizationCustomPermissionKey[] | undefined;
289-
/**
290-
* Factor Verification Age
291-
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
292-
* [fistFactorAge, secondFactorAge]
293-
*/
294-
factorVerificationAge: [firstFactorAge: number, secondFactorAge: number] | null;
295-
};
296-
297-
/**
298-
* @experimental
299-
*
300-
* Resolves the signed-in auth state from JWT claims.
301-
*/
302-
const __experimental_resolveSignedInAuthStateFromJWTClaims = (claims: JwtPayload): SignedInAuthObjectProperties => {
303-
let orgId: string | undefined;
304-
let orgRole: OrganizationCustomRoleKey | undefined;
305-
let orgSlug: string | undefined;
306-
let orgPermissions: OrganizationCustomPermissionKey[] | undefined;
307-
308-
// fva can be undefined for instances that have not opt-in
309-
const factorVerificationAge = claims.fva ?? null;
310-
311-
// sts can be undefined for instances that have not opt-in
312-
const sessionStatus = claims.sts ?? null;
313-
314-
switch (claims.v) {
315-
case 2: {
316-
orgId = claims.org?.id;
317-
orgRole = claims.org?.rol;
318-
orgSlug = claims.org?.slg;
319-
orgPermissions = claims.org?.per?.split(',').map((permission: string) => permission.trim()) || undefined;
320-
break;
321-
}
322-
default:
323-
orgId = claims.org_id;
324-
orgRole = claims.org_role;
325-
orgSlug = claims.org_slug;
326-
orgPermissions = claims.org_permissions;
327-
break;
328-
}
329-
330-
return {
331-
sessionClaims: claims,
332-
sessionId: claims.sid,
333-
sessionStatus,
334-
actor: claims.act,
335-
userId: claims.sub,
336-
orgId: orgId,
337-
orgRole: orgRole,
338-
orgSlug: orgSlug,
339-
orgPermissions,
340-
factorVerificationAge,
341-
};
342-
};
343-
344-
export {
345-
createCheckAuthorization,
346-
validateReverificationConfig,
347-
resolveAuthState,
348-
__experimental_resolveSignedInAuthStateFromJWTClaims,
349-
};
276+
export { createCheckAuthorization, validateReverificationConfig, resolveAuthState };

0 commit comments

Comments
 (0)