Skip to content

Commit c63a5ad

Browse files
feat(backend,types,clerk-js,clerk-react): Add the fva sessionClaim (#4061)
Co-authored-by: Haris Chaniotakis <haris@clerk.dev>
1 parent f5610f1 commit c63a5ad

File tree

12 files changed

+112
-9
lines changed

12 files changed

+112
-9
lines changed

.changeset/eleven-apricots-rest.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@clerk/clerk-js": minor
3+
"@clerk/backend": minor
4+
"@clerk/clerk-react": minor
5+
"@clerk/types": minor
6+
---
7+
8+
Experimental support: Expect a new sessionClaim called `fva` that tracks the age of verified factor groups.
9+
10+
### Server side
11+
12+
This can be applied to any helper that returns the auth object
13+
14+
**Nextjs example**
15+
16+
```ts
17+
auth(). __experimental_factorVerificationAge
18+
```
19+
20+
### Client side
21+
22+
**React example**
23+
```ts
24+
const { session } = useSession()
25+
session?. __experimental_factorVerificationAge
26+
```

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/tokens/authObjects.ts

+17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export type SignedInAuthObject = {
3434
orgRole: OrganizationCustomRoleKey | undefined;
3535
orgSlug: string | undefined;
3636
orgPermissions: OrganizationCustomPermissionKey[] | undefined;
37+
/**
38+
* Factor Verification Age
39+
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
40+
* [fistFactorAge, secondFactorAge]
41+
* @experimental This API is experimental and may change at any moment.
42+
*/
43+
__experimental_factorVerificationAge: [number | null, number | null];
3744
getToken: ServerGetToken;
3845
has: CheckAuthorizationWithCustomPermissions;
3946
debug: AuthObjectDebug;
@@ -51,6 +58,13 @@ export type SignedOutAuthObject = {
5158
orgRole: null;
5259
orgSlug: null;
5360
orgPermissions: null;
61+
/**
62+
* Factor Verification Age
63+
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
64+
* [fistFactorAge, secondFactorAge]
65+
* @experimental This API is experimental and may change at any moment.
66+
*/
67+
__experimental_factorVerificationAge: [null, null];
5468
getToken: ServerGetToken;
5569
has: CheckAuthorizationWithCustomPermissions;
5670
debug: AuthObjectDebug;
@@ -86,6 +100,7 @@ export function signedInAuthObject(
86100
org_slug: orgSlug,
87101
org_permissions: orgPermissions,
88102
sub: userId,
103+
fva: __experimental_factorVerificationAge,
89104
} = sessionClaims;
90105
const apiClient = createBackendApiClient(authenticateContext);
91106
const getToken = createGetToken({
@@ -103,6 +118,7 @@ export function signedInAuthObject(
103118
orgRole,
104119
orgSlug,
105120
orgPermissions,
121+
__experimental_factorVerificationAge,
106122
getToken,
107123
has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }),
108124
debug: createDebug({ ...authenticateContext, sessionToken }),
@@ -122,6 +138,7 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
122138
orgRole: null,
123139
orgSlug: null,
124140
orgPermissions: null,
141+
__experimental_factorVerificationAge: [null, null],
125142
getToken: () => Promise.resolve(null),
126143
has: () => false,
127144
debug: createDebug(debugData),

packages/clerk-js/src/core/resources/Session.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class Session extends BaseResource implements SessionResource {
3939
actor!: ActJWTClaim | null;
4040
user!: UserResource | null;
4141
publicUserData!: PublicUserData;
42+
__experimental_factorVerificationAge: [number | null, number | null] = [null, null];
4243
expireAt!: Date;
4344
abandonAt!: Date;
4445
createdAt!: Date;
@@ -235,6 +236,7 @@ export class Session extends BaseResource implements SessionResource {
235236
this.status = data.status;
236237
this.expireAt = unixEpochToDate(data.expire_at);
237238
this.abandonAt = unixEpochToDate(data.abandon_at);
239+
this.__experimental_factorVerificationAge = data.factor_verification_age;
238240
this.lastActiveAt = unixEpochToDate(data.last_active_at);
239241
this.lastActiveOrganizationId = data.last_active_organization_id;
240242
this.actor = data.actor;

packages/clerk-js/src/utils/memoizeStateListenerCallback.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ function sessionChanged(prev: SessionResource, next: SessionResource): boolean {
3131
return (
3232
prev.id !== next.id ||
3333
prev.updatedAt.getTime() < next.updatedAt.getTime() ||
34-
sessionUserMembershipPermissionsChanged(next, prev)
34+
sessionFVAChanged(prev, next) ||
35+
sessionUserMembershipPermissionsChanged(prev, next)
3536
);
3637
}
3738

@@ -49,6 +50,13 @@ function userMembershipsChanged(prev: UserResource, next: UserResource): boolean
4950
);
5051
}
5152

53+
function sessionFVAChanged(prev: SessionResource, next: SessionResource): boolean {
54+
return (
55+
prev.__experimental_factorVerificationAge[0] !== next.__experimental_factorVerificationAge[0] ||
56+
prev.__experimental_factorVerificationAge[1] !== next.__experimental_factorVerificationAge[1]
57+
);
58+
}
59+
5260
function sessionUserMembershipPermissionsChanged(prev: SessionResource, next: SessionResource): boolean {
5361
if (prev.lastActiveOrganizationId !== next.lastActiveOrganizationId) {
5462
return true;

packages/react/src/contexts/AuthContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{
99
orgRole: OrganizationCustomRoleKey | null | undefined;
1010
orgSlug: string | null | undefined;
1111
orgPermissions: OrganizationCustomPermissionKey[] | null | undefined;
12+
__experimental_factorVerificationAge: [number | null, number | null];
1213
}>('AuthContext');

packages/react/src/contexts/ClerkContextProvider.tsx

+25-5
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,35 @@ export function ClerkContextProvider(props: ClerkContextProvider): JSX.Element |
3535
const clerkCtx = React.useMemo(() => ({ value: clerk }), [clerkLoaded]);
3636
const clientCtx = React.useMemo(() => ({ value: state.client }), [state.client]);
3737

38-
const { sessionId, session, userId, user, orgId, actor, organization, orgRole, orgSlug, orgPermissions } =
39-
derivedState;
38+
const {
39+
sessionId,
40+
session,
41+
userId,
42+
user,
43+
orgId,
44+
actor,
45+
organization,
46+
orgRole,
47+
orgSlug,
48+
orgPermissions,
49+
__experimental_factorVerificationAge,
50+
} = derivedState;
4051

4152
const authCtx = React.useMemo(() => {
42-
const value = { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions };
53+
const value = {
54+
sessionId,
55+
userId,
56+
actor,
57+
orgId,
58+
orgRole,
59+
orgSlug,
60+
orgPermissions,
61+
__experimental_factorVerificationAge,
62+
};
4363
return { value };
44-
}, [sessionId, userId, actor, orgId, orgRole, orgSlug]);
45-
const userCtx = React.useMemo(() => ({ value: user }), [userId, user]);
64+
}, [sessionId, userId, actor, orgId, orgRole, orgSlug, __experimental_factorVerificationAge]);
4665
const sessionCtx = React.useMemo(() => ({ value: session }), [sessionId, session]);
66+
const userCtx = React.useMemo(() => ({ value: user }), [userId, user]);
4767
const organizationCtx = React.useMemo(() => {
4868
const value = {
4969
organization: organization,

packages/react/src/utils/deriveState.ts

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => {
2626
const orgPermissions = initialState.orgPermissions as OrganizationCustomPermissionKey[];
2727
const orgSlug = initialState.orgSlug;
2828
const actor = initialState.actor;
29+
const __experimental_factorVerificationAge = initialState.__experimental_factorVerificationAge;
2930

3031
return {
3132
userId,
@@ -38,6 +39,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => {
3839
orgPermissions,
3940
orgSlug,
4041
actor,
42+
__experimental_factorVerificationAge,
4143
};
4244
};
4345

@@ -46,6 +48,9 @@ const deriveFromClientSideState = (state: Resources) => {
4648
const user = state.user;
4749
const sessionId: string | null | undefined = state.session ? state.session.id : state.session;
4850
const session = state.session;
51+
const __experimental_factorVerificationAge: [number | null, number | null] = state.session
52+
? state.session.__experimental_factorVerificationAge
53+
: [null, null];
4954
const actor = session?.actor;
5055
const organization = state.organization;
5156
const orgId: string | null | undefined = state.organization ? state.organization.id : state.organization;
@@ -67,5 +72,6 @@ const deriveFromClientSideState = (state: Resources) => {
6772
orgSlug,
6873
orgPermissions,
6974
actor,
75+
__experimental_factorVerificationAge,
7076
};
7177
};

packages/types/src/json.ts

+7
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ export interface SessionJSON extends ClerkResourceJSON {
104104
object: 'session';
105105
id: string;
106106
status: SessionStatus;
107+
/**
108+
* Factor Verification Age
109+
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
110+
* [fistFactorAge, secondFactorAge]
111+
* @experimental This API is experimental and may change at any moment.
112+
*/
113+
factor_verification_age: [number | null, number | null];
107114
expire_at: number;
108115
abandon_at: number;
109116
last_active_at: number;

packages/types/src/jwtv2.ts

+8
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export interface JwtPayload extends CustomJwtSessionClaims {
101101
*/
102102
org_permissions?: OrganizationCustomPermissionKey[];
103103

104+
/**
105+
* Factor Verification Age
106+
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
107+
* [fistFactorAge, secondFactorAge]
108+
* @experimental This API is experimental and may change at any moment.
109+
*/
110+
fva: [number | null, number | null];
111+
104112
/**
105113
* Any other JWT Claim Set member.
106114
*/

packages/types/src/session.ts

+7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export interface SessionResource extends ClerkResource {
5555
status: SessionStatus;
5656
expireAt: Date;
5757
abandonAt: Date;
58+
/**
59+
* Factor Verification Age
60+
* Each item represents the minutes that have passed since the last time a first or second factor were verified.
61+
* [fistFactorAge, secondFactorAge]
62+
* @experimental This API is experimental and may change at any moment.
63+
*/
64+
__experimental_factorVerificationAge: [number | null, number | null];
5865
lastActiveToken: TokenResource | null;
5966
lastActiveOrganizationId: string | null;
6067
lastActiveAt: Date;

packages/types/src/ssr.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export type InitialState = Serializable<{
2020
orgSlug: string | undefined;
2121
orgPermissions: OrganizationCustomPermissionKey[] | undefined;
2222
organization: OrganizationResource | undefined;
23+
__experimental_factorVerificationAge: [number | null, number | null];
2324
}>;

0 commit comments

Comments
 (0)