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
8 changes: 8 additions & 0 deletions .changeset/cuddly-cars-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

Introduce `sessionClaims` to useAuth().
- thanks to [@ijxy](https://github.com/ijxy) for the [contribution](https://github.com/clerk/javascript/pull/4823)
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "590kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "73.4KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "73.5KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "99KB" },
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ export class Clerk implements ClerkInterface {

this.#options = this.#initOptions(options);

/**
* Listen to `Session.getToken` resolving to emit the updated session
* with the new token to the state listeners.
*/
eventBus.on(events.SessionTokenResolved, () => {
this.#setAccessors(this.session);
this.#emit();
});

assertNoLegacyProp(this.#options);

if (this.#options.sdkMetadata) {
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const events = {
TokenUpdate: 'token:update',
UserSignOut: 'user:signOut',
EnvironmentUpdate: 'environment:update',
SessionTokenResolved: 'session:tokenResolved',
} as const;

type TokenUpdatePayload = { token: TokenResource | null };
Expand All @@ -13,6 +14,7 @@ type InternalEvents = {
[events.TokenUpdate]: TokenUpdatePayload;
[events.UserSignOut]: null;
[events.EnvironmentUpdate]: null;
[events.SessionTokenResolved]: null;
};

export const eventBus = createEventBus<InternalEvents>();
6 changes: 6 additions & 0 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ export class Session extends BaseResource implements SessionResource {
return tokenResolver.then(token => {
if (shouldDispatchTokenUpdate) {
eventBus.emit(events.TokenUpdate, { token });

if (token.jwt) {
this.lastActiveToken = token;
// Emits the updated session with the new token to the state listeners
eventBus.emit(events.SessionTokenResolved, null);
}
}
// Return null when raw string is empty to indicate that there it's signed-out
return token.getRawString() || null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ describe('Session', () => {

await session.getToken();

expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(2);
expect(dispatchSpy.mock.calls[0]).toMatchSnapshot();
expect(dispatchSpy.mock.calls[1]).toMatchSnapshot();
});

it('hydrates token cache from lastActiveToken', async () => {
Expand Down Expand Up @@ -95,8 +96,9 @@ describe('Session', () => {

await session.getToken();

expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(2);
expect(dispatchSpy.mock.calls[0]).toMatchSnapshot();
expect(dispatchSpy.mock.calls[1]).toMatchSnapshot();
});

it('does not dispatch token:update if template is provided', async () => {
Expand Down Expand Up @@ -138,7 +140,7 @@ describe('Session', () => {

await session.getToken({ organizationId: 'activeOrganization' });

expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(2);
});

it('does not dispatch token:update when provided organization ID does not match current active organization', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ exports[`Session getToken() dispatches token:update event on getToken with activ
]
`;

exports[`Session getToken() dispatches token:update event on getToken with active organization 2`] = `
[
"session:tokenResolved",
null,
]
`;

exports[`Session getToken() dispatches token:update event on getToken without active organization 1`] = `
[
"token:update",
Expand Down Expand Up @@ -67,3 +74,10 @@ exports[`Session getToken() dispatches token:update event on getToken without ac
},
]
`;

exports[`Session getToken() dispatches token:update event on getToken without active organization 2`] = `
[
"session:tokenResolved",
null,
]
`;
17 changes: 3 additions & 14 deletions packages/clerk-js/src/utils/memoizeStateListenerCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ function sessionChanged(prev: SessionResource, next: SessionResource): boolean {
return (
prev.id !== next.id ||
prev.updatedAt.getTime() < next.updatedAt.getTime() ||
sessionFVAChanged(prev, next) ||
// TODO: Optimize this to once JWT v2 formatting is out.
prev.lastActiveToken?.jwt?.claims?.__raw !== next.lastActiveToken?.jwt?.claims?.__raw ||
sessionUserMembershipPermissionsChanged(prev, next) ||
sessionUserChanged(prev, next)
);
Expand All @@ -53,15 +54,6 @@ function userMembershipsChanged(prev: UserResource, next: UserResource): boolean
);
}

function sessionFVAChanged(prev: SessionResource, next: SessionResource): boolean {
const prevFVA = prev.factorVerificationAge;
const nextFVA = next.factorVerificationAge;
if (prevFVA !== null && nextFVA !== null) {
return prevFVA[0] !== nextFVA[0] || prevFVA[1] !== nextFVA[1];
}
return prevFVA !== nextFVA;
}

function sessionUserChanged(prev: SessionResource, next: SessionResource): boolean {
if (!!prev.user !== !!next.user) {
return true;
Expand All @@ -82,10 +74,7 @@ function sessionUserMembershipPermissionsChanged(prev: SessionResource, next: Se
mem => mem.organization.id === prev.lastActiveOrganizationId,
);

return (
prevActiveMembership?.role !== nextActiveMembership?.role ||
prevActiveMembership?.permissions?.length !== nextActiveMembership?.permissions?.length
);
return prevActiveMembership?.permissions?.length !== nextActiveMembership?.permissions?.length;
}

// TODO: Decide if this belongs in the resources
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/contexts/AuthContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContextAndHook } from '@clerk/shared/react';
import type {
ActClaim,
JwtPayload,
OrganizationCustomPermissionKey,
OrganizationCustomRoleKey,
SessionStatusClaim,
Expand All @@ -10,6 +11,7 @@ export type AuthContextValue = {
userId: string | null | undefined;
sessionId: string | null | undefined;
sessionStatus: SessionStatusClaim | null | undefined;
sessionClaims: JwtPayload | null | undefined;
actor: ActClaim | null | undefined;
orgId: string | null | undefined;
orgRole: OrganizationCustomRoleKey | null | undefined;
Expand Down
5 changes: 4 additions & 1 deletion packages/react/src/contexts/ClerkContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
const {
sessionId,
sessionStatus,
sessionClaims,
session,
userId,
user,
Expand All @@ -54,6 +55,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
const value = {
sessionId,
sessionStatus,
sessionClaims,
userId,
actor,
orgId,
Expand All @@ -63,7 +65,8 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
factorVerificationAge,
};
return { value };
}, [sessionId, sessionStatus, userId, actor, orgId, orgRole, orgSlug, factorVerificationAge]);
}, [sessionId, sessionStatus, userId, actor, orgId, orgRole, orgSlug, factorVerificationAge, sessionClaims?.__raw]);

const sessionCtx = React.useMemo(() => ({ value: session }), [sessionId, session]);
const userCtx = React.useMemo(() => ({ value: user }), [userId, user]);
const organizationCtx = React.useMemo(() => {
Expand Down
31 changes: 29 additions & 2 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createCheckAuthorization } from '@clerk/shared/authorization';
import { ClerkInstanceContext } from '@clerk/shared/react';
import type { LoadedClerk } from '@clerk/types';
import type { LoadedClerk, UseAuthReturn } from '@clerk/types';
import { render, renderHook } from '@testing-library/react';
import React from 'react';
import { afterAll, beforeAll, beforeEach, describe, expect, expectTypeOf, it, test, vi } from 'vitest';
Expand Down Expand Up @@ -34,6 +34,21 @@ const TestComponent = () => {
);
};

const stubSessionClaims = (input: {
sessionId: string;
userId: string;
orgId?: string;
}): NonNullable<UseAuthReturn['sessionClaims']> => ({
__raw: '',
exp: 1,
iat: 1,
iss: '',
nbf: 1,
sid: input.sessionId,
sub: input.userId,
org_id: input.orgId,
});

describe('useAuth', () => {
let consoleErrorSpy: any;

Expand Down Expand Up @@ -77,6 +92,7 @@ describe('useDerivedAuth', () => {
expect(current.isLoaded).toBe(false);
expect(current.isSignedIn).toBeUndefined();
expect(current.sessionId).toBeUndefined();
expect(current.sessionClaims).toBeUndefined();
expect(current.userId).toBeUndefined();
expect(current.actor).toBeUndefined();
expect(current.orgId).toBeUndefined();
Expand All @@ -92,6 +108,7 @@ describe('useDerivedAuth', () => {
expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.sessionClaims).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
Expand Down Expand Up @@ -134,6 +151,7 @@ describe('useDerivedAuth', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123', orgId: 'org123' }),
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
Expand Down Expand Up @@ -163,6 +181,7 @@ describe('useDerivedAuth', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123', orgId: 'org123' }),
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
Expand Down Expand Up @@ -192,6 +211,7 @@ describe('useDerivedAuth', () => {
it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123', orgId: 'org123' }),
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
Expand All @@ -208,6 +228,9 @@ describe('useDerivedAuth', () => {
expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.sessionClaims?.sid).toBe(current.sessionId);
expect(current.sessionClaims?.sub).toBe(current.userId);
expect(current.sessionClaims?.org_id).toBe(current.orgId);
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
Expand All @@ -225,6 +248,7 @@ describe('useDerivedAuth', () => {
it('returns signed in without org context when sessionId and userId are present but no orgId', () => {
const authObject = {
sessionId: 'session123',
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123' }),
userId: 'user123',
actor: 'actor123',
signOut: vi.fn(),
Expand All @@ -237,6 +261,8 @@ describe('useDerivedAuth', () => {
expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.sessionClaims?.sid).toBe(current.sessionId);
expect(current.sessionClaims?.sub).toBe(current.userId);
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBeNull();
Expand All @@ -253,7 +279,7 @@ describe('useDerivedAuth', () => {

it('throws invalid state error if none of the conditions match', () => {
const authObject = {
sessionId: true,
sessionId: 'session123',
userId: undefined,
};
renderHook(() => useDerivedAuth(authObject));
Expand All @@ -267,6 +293,7 @@ describe('useDerivedAuth', () => {
const authObject = {
sessionId: 'session123',
userId: 'user123',
sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123' }),
has: mockHas,
};
const {
Expand Down
Loading