diff --git a/.changeset/honest-oranges-accept.md b/.changeset/honest-oranges-accept.md new file mode 100644 index 00000000000..94e7dfa4f38 --- /dev/null +++ b/.changeset/honest-oranges-accept.md @@ -0,0 +1,8 @@ +--- +'@clerk/astro': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +'@clerk/vue': patch +--- + +Expose `sessionClaims` from `useAuth` hooks diff --git a/packages/astro/src/react/hooks.ts b/packages/astro/src/react/hooks.ts index e8b99e1c358..7950377d58e 100644 --- a/packages/astro/src/react/hooks.ts +++ b/packages/astro/src/react/hooks.ts @@ -1,11 +1,4 @@ -import type { - ActJWTClaim, - CheckAuthorizationWithCustomPermissions, - Clerk, - GetToken, - OrganizationCustomRoleKey, - SignOut, -} from '@clerk/types'; +import type { Clerk, GetToken, SignOut, UseAuthReturn } from '@clerk/types'; import type { Store, StoreValue } from 'nanostores'; import { useCallback, useSyncExternalStore } from 'react'; @@ -15,9 +8,6 @@ import { authAsyncStorage } from '#async-local-storage'; import { $authStore } from '../stores/external'; import { $clerk, $csrState } from '../stores/internal'; -type CheckAuthorizationSignedOut = undefined; -type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters[0]) => false; - /** * @internal */ @@ -54,60 +44,6 @@ const createSignOut = () => { }; }; -type UseAuthReturn = - | { - isLoaded: false; - isSignedIn: undefined; - userId: undefined; - sessionId: undefined; - actor: undefined; - orgId: undefined; - orgRole: undefined; - orgSlug: undefined; - has: CheckAuthorizationSignedOut; - signOut: SignOut; - getToken: GetToken; - } - | { - isLoaded: true; - isSignedIn: false; - userId: null; - sessionId: null; - actor: null; - orgId: null; - orgRole: null; - orgSlug: null; - has: CheckAuthorizationWithoutOrgOrUser; - signOut: SignOut; - getToken: GetToken; - } - | { - isLoaded: true; - isSignedIn: true; - userId: string; - sessionId: string; - actor: ActJWTClaim | null; - orgId: null; - orgRole: null; - orgSlug: null; - has: CheckAuthorizationWithoutOrgOrUser; - signOut: SignOut; - getToken: GetToken; - } - | { - isLoaded: true; - isSignedIn: true; - userId: string; - sessionId: string; - actor: ActJWTClaim | null; - orgId: string; - orgRole: OrganizationCustomRoleKey; - orgSlug: string | null; - has: CheckAuthorizationWithCustomPermissions; - signOut: SignOut; - getToken: GetToken; - }; - type UseAuth = () => UseAuthReturn; /** @@ -142,13 +78,13 @@ type UseAuth = () => UseAuthReturn; * } */ export const useAuth: UseAuth = () => { - const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore); + const { sessionId, sessionClaims, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore); const getToken: GetToken = useCallback(createGetToken(), []); const signOut: SignOut = useCallback(createSignOut(), []); - const has = useCallback( - (params: Parameters[0]) => { + const has = useCallback>( + params => { if (!params?.permission && !params?.role) { throw new Error( 'Missing parameters. `has` from `useAuth` requires a permission or role key to be passed. Example usage: `has({permission: "org:posts:edit"`', @@ -177,6 +113,7 @@ export const useAuth: UseAuth = () => { isLoaded: false, isSignedIn: undefined, sessionId, + sessionClaims: undefined, userId, actor: undefined, orgId: undefined, @@ -193,6 +130,7 @@ export const useAuth: UseAuth = () => { isLoaded: true, isSignedIn: false, sessionId, + sessionClaims: null, userId, actor: null, orgId: null, @@ -204,11 +142,12 @@ export const useAuth: UseAuth = () => { }; } - if (!!sessionId && !!userId && !!orgId && !!orgRole) { + if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId, @@ -220,11 +159,12 @@ export const useAuth: UseAuth = () => { }; } - if (!!sessionId && !!userId && !orgId) { + if (!!sessionId && !!sessionClaims && !!userId && !orgId) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId: null, diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index 02d39196200..bed3baf52bd 100644 --- a/packages/react/src/contexts/AuthContext.ts +++ b/packages/react/src/contexts/AuthContext.ts @@ -1,9 +1,10 @@ import { createContextAndHook } from '@clerk/shared/react'; -import type { ActJWTClaim, OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from '@clerk/types'; +import type { ActJWTClaim, JwtPayload, OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from '@clerk/types'; export type AuthContextValue = { userId: string | null | undefined; sessionId: string | null | undefined; + sessionClaims: JwtPayload | null | undefined; actor: ActJWTClaim | null | undefined; orgId: string | null | undefined; orgRole: OrganizationCustomRoleKey | null | undefined; diff --git a/packages/react/src/hooks/__tests__/useAuth.test.tsx b/packages/react/src/hooks/__tests__/useAuth.test.tsx index b709f46781e..8be10379623 100644 --- a/packages/react/src/hooks/__tests__/useAuth.test.tsx +++ b/packages/react/src/hooks/__tests__/useAuth.test.tsx @@ -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'; @@ -24,6 +24,21 @@ vi.mock('../../errors/errorThrower', () => ({ }, })); +const stubSessionClaims = (input: { + sessionId: string; + userId: string; + orgId?: string; +}): NonNullable => ({ + __raw: '', + exp: 1, + iat: 1, + iss: '', + nbf: 1, + sid: input.sessionId, + sub: input.userId, + org_id: input.orgId, +}); + const TestComponent = () => { const { isLoaded, isSignedIn } = useAuth(); return ( @@ -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(); @@ -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(); @@ -104,14 +121,15 @@ 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' }), userId: 'user123', - actor: 'actor123', + actor: { sub: 'actor123' }, orgId: 'org123', orgRole: 'admin', orgSlug: 'my-org', signOut: vi.fn(), getToken: vi.fn(), - }; + } satisfies Partial; const { result: { current }, @@ -119,12 +137,13 @@ describe('useDerivedAuth', () => { expect(current.isLoaded).toBe(true); expect(current.isSignedIn).toBe(true); - expect(current.sessionId).toBe('session123'); - expect(current.userId).toBe('user123'); - expect(current.actor).toBe('actor123'); - expect(current.orgId).toBe('org123'); - expect(current.orgRole).toBe('admin'); - expect(current.orgSlug).toBe('my-org'); + expect(current.sessionId).toBe(authObject.sessionId); + expect(current.userId).toBe(authObject.userId); + expect(current.sessionClaims).toBe(authObject.sessionClaims); + expect(current.actor?.sub).toBe(authObject.actor.sub); + expect(current.orgId).toBe(authObject.orgId); + expect(current.orgRole).toBe(authObject.orgRole); + expect(current.orgSlug).toBe(authObject.orgSlug); expect(typeof current.has).toBe('function'); expect(current.signOut).toBe(authObject.signOut); expect(current.getToken).toBe(authObject.getToken); @@ -136,21 +155,23 @@ describe('useDerivedAuth', () => { it('returns signed in without org context when sessionId and userId are present but no orgId', () => { const authObject = { - sessionId: 'session123', userId: 'user123', - actor: 'actor123', + sessionId: 'session123', + sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123' }), + actor: { sub: 'actor123' }, signOut: vi.fn(), getToken: vi.fn(), - }; + } satisfies Partial; const { result: { current }, } = renderHook(() => useDerivedAuth(authObject)); expect(current.isLoaded).toBe(true); expect(current.isSignedIn).toBe(true); - expect(current.sessionId).toBe('session123'); - expect(current.userId).toBe('user123'); - expect(current.actor).toBe('actor123'); + expect(current.sessionId).toBe(authObject.sessionId); + expect(current.userId).toBe(authObject.userId); + expect(current.sessionClaims).toBe(authObject.sessionClaims); + expect(current.actor?.sub).toBe(authObject.actor.sub); expect(current.orgId).toBeNull(); expect(current.orgRole).toBeNull(); expect(current.orgSlug).toBeNull(); @@ -165,9 +186,9 @@ describe('useDerivedAuth', () => { it('throws invalid state error if none of the conditions match', () => { const authObject = { - sessionId: true, userId: undefined, - }; + sessionId: 'session123', + } satisfies Partial; renderHook(() => useDerivedAuth(authObject)); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -177,10 +198,11 @@ describe('useDerivedAuth', () => { it('uses provided has function if available', () => { const mockHas = vi.fn().mockReturnValue(false); const authObject = { - sessionId: 'session123', userId: 'user123', + sessionId: 'session123', + sessionClaims: stubSessionClaims({ sessionId: 'session123', userId: 'user123' }), has: mockHas, - }; + } satisfies Partial; const { result: { current }, } = renderHook(() => useDerivedAuth(authObject)); diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 4598fd4226b..6af4d5020bb 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -57,7 +57,8 @@ export const useAuth: UseAuth = (initialAuthState = {}) => { authContext = initialAuthState != null ? initialAuthState : {}; } - const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions, factorVerificationAge } = authContext; + const { sessionId, sessionClaims, userId, actor, orgId, orgRole, orgSlug, orgPermissions, factorVerificationAge } = + authContext; const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -65,6 +66,7 @@ export const useAuth: UseAuth = (initialAuthState = {}) => { return useDerivedAuth({ sessionId, + sessionClaims, userId, actor, orgId, @@ -106,6 +108,7 @@ export const useAuth: UseAuth = (initialAuthState = {}) => { export function useDerivedAuth(authObject: any): UseAuthReturn { const { sessionId, + sessionClaims, userId, actor, orgId, @@ -139,6 +142,7 @@ export function useDerivedAuth(authObject: any): UseAuthReturn { isLoaded: false, isSignedIn: undefined, sessionId, + sessionClaims: undefined, userId, actor: undefined, orgId: undefined, @@ -155,6 +159,7 @@ export function useDerivedAuth(authObject: any): UseAuthReturn { isLoaded: true, isSignedIn: false, sessionId, + sessionClaims: null, userId, actor: null, orgId: null, @@ -166,11 +171,12 @@ export function useDerivedAuth(authObject: any): UseAuthReturn { }; } - if (!!sessionId && !!userId && !!orgId && !!orgRole) { + if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId, @@ -182,11 +188,12 @@ export function useDerivedAuth(authObject: any): UseAuthReturn { }; } - if (!!sessionId && !!userId && !orgId) { + if (!!sessionId && !!sessionClaims && !!userId && !orgId) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId: null, diff --git a/packages/shared/src/__tests__/deriveState.test.ts b/packages/shared/src/__tests__/deriveState.test.ts index 244cb3fe857..8b11141e29a 100644 --- a/packages/shared/src/__tests__/deriveState.test.ts +++ b/packages/shared/src/__tests__/deriveState.test.ts @@ -3,17 +3,27 @@ import type { InitialState, Resources } from '@clerk/types'; import { deriveState } from '../deriveState'; describe('deriveState', () => { + const mockSessionClaims = { + sid: 'sess_2j1R7g3AUeKMx9M23dBO0XLEQGY', + sub: 'user_2U330vGHg3llBga8Oi0fzzeNAaG', + org_id: 'org_2U330vGHg3llBga8Oi0fzzeNAaG', + } as InitialState['sessionClaims']; + const mockInitialState = { - userId: 'user_2U330vGHg3llBga8Oi0fzzeNAaG', - sessionId: 'sess_2j1R7g3AUeKMx9M23dBO0XLEQGY', - orgId: 'org_2U330vGHg3llBga8Oi0fzzeNAaG', + userId: mockSessionClaims.sub, + orgId: mockSessionClaims.org_id, + sessionId: mockSessionClaims.sid, + sessionClaims: mockSessionClaims, } as InitialState; const mockResources = { client: {}, user: { id: mockInitialState.userId }, - session: { id: mockInitialState.sessionId }, organization: { id: mockInitialState.orgId }, + session: { + id: mockInitialState.sessionId, + lastActiveToken: { jwt: { claims: mockSessionClaims } }, + }, } as Resources; it('uses SSR state when !clerkLoaded and initialState is provided', () => { @@ -25,6 +35,9 @@ describe('deriveState', () => { expect(result.userId).toBe(mockInitialState.userId); expect(result.sessionId).toBe(mockInitialState.sessionId); expect(result.orgId).toBe(mockInitialState.orgId); + expect(result.sessionClaims?.sid).toBe(mockInitialState.sessionId); + expect(result.sessionClaims?.sub).toBe(mockInitialState.userId); + expect(result.sessionClaims?.org_id).toBe(mockInitialState.orgId); }); it('handles !clerkLoaded and undefined initialState', () => { @@ -32,5 +45,6 @@ describe('deriveState', () => { expect(result.userId).toBeUndefined(); expect(result.sessionId).toBeUndefined(); expect(result.orgId).toBeUndefined(); + expect(result.sessionClaims).toBeUndefined(); }); }); diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts index 771c8040839..9573e446bdd 100644 --- a/packages/shared/src/deriveState.ts +++ b/packages/shared/src/deriveState.ts @@ -1,6 +1,7 @@ import type { ActiveSessionResource, InitialState, + JwtPayload, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, @@ -23,6 +24,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const user = initialState.user as UserResource; const sessionId = initialState.sessionId; const session = initialState.session as ActiveSessionResource; + const sessionClaims = initialState.sessionClaims; const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as OrganizationCustomRoleKey; @@ -36,6 +38,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { user, sessionId, session, + sessionClaims, organization, orgId, orgRole, @@ -51,6 +54,9 @@ const deriveFromClientSideState = (state: Resources) => { const user = state.user; const sessionId: string | null | undefined = state.session ? state.session.id : state.session; const session = state.session; + const sessionClaims: JwtPayload | null | undefined = state.session + ? state.session.lastActiveToken?.jwt?.claims + : state.session; const factorVerificationAge: [number, number] | null = state.session ? state.session.factorVerificationAge : null; const actor = session?.actor; const organization = state.organization; @@ -67,6 +73,7 @@ const deriveFromClientSideState = (state: Resources) => { user, sessionId, session, + sessionClaims, organization, orgId, orgRole, diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 1b5d4ab0b0c..1e02915c735 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -1,14 +1,14 @@ -import type { OrganizationCustomRoleKey } from 'organizationMembership'; -import type { SignInResource } from 'signIn'; - import type { SetActive, SignOut } from './clerk'; import type { ActJWTClaim } from './jwt'; +import type { JwtPayload } from './jwtv2'; +import type { OrganizationCustomRoleKey } from './organizationMembership'; import type { ActiveSessionResource, CheckAuthorizationWithCustomPermissions, GetToken, SessionResource, } from './session'; +import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { UserResource } from './user'; @@ -21,6 +21,7 @@ export type UseAuthReturn = isSignedIn: undefined; userId: undefined; sessionId: undefined; + sessionClaims: undefined; actor: undefined; orgId: undefined; orgRole: undefined; @@ -34,6 +35,7 @@ export type UseAuthReturn = isSignedIn: false; userId: null; sessionId: null; + sessionClaims: null; actor: null; orgId: null; orgRole: null; @@ -47,6 +49,7 @@ export type UseAuthReturn = isSignedIn: true; userId: string; sessionId: string; + sessionClaims: JwtPayload; actor: ActJWTClaim | null; orgId: null; orgRole: null; @@ -60,6 +63,7 @@ export type UseAuthReturn = isSignedIn: true; userId: string; sessionId: string; + sessionClaims: JwtPayload; actor: ActJWTClaim | null; orgId: string; orgRole: OrganizationCustomRoleKey; diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index a6db48b5f72..3b7f945deff 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -1,4 +1,4 @@ -import type { InstanceType } from 'instance'; +import type { InstanceType } from './instance'; /** * @internal diff --git a/packages/vue/src/composables/useAuth.ts b/packages/vue/src/composables/useAuth.ts index 1c4da2f9a75..9ecb863455a 100644 --- a/packages/vue/src/composables/useAuth.ts +++ b/packages/vue/src/composables/useAuth.ts @@ -80,7 +80,7 @@ export const useAuth: UseAuth = () => { const signOut: SignOut = createSignOut(clerk); const result = computed(() => { - const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = authCtx.value; + const { sessionId, sessionClaims, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = authCtx.value; const has = (params: Parameters[0]) => { if (!params?.permission && !params?.role) { @@ -106,6 +106,7 @@ export const useAuth: UseAuth = () => { isLoaded: false, isSignedIn: undefined, sessionId, + sessionClaims: undefined, userId, actor: undefined, orgId: undefined, @@ -122,6 +123,7 @@ export const useAuth: UseAuth = () => { isLoaded: true, isSignedIn: false, sessionId, + sessionClaims: null, userId, actor: null, orgId: null, @@ -133,11 +135,12 @@ export const useAuth: UseAuth = () => { }; } - if (!!sessionId && !!userId && !!orgId && !!orgRole) { + if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId, @@ -149,11 +152,12 @@ export const useAuth: UseAuth = () => { }; } - if (!!sessionId && !!userId && !orgId) { + if (!!sessionId && !!sessionClaims && !!userId && !orgId) { return { isLoaded: true, isSignedIn: true, sessionId, + sessionClaims, userId, actor: actor || null, orgId: null, diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts index ab5acddaffc..92b97e0e294 100644 --- a/packages/vue/src/plugin.ts +++ b/packages/vue/src/plugin.ts @@ -70,8 +70,8 @@ export const clerkPlugin: Plugin = { const derivedState = computed(() => deriveState(loaded.value, resources.value, initialState)); const authCtx = computed(() => { - const { sessionId, userId, orgId, actor, orgRole, orgSlug, orgPermissions } = derivedState.value; - return { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions }; + const { sessionId, sessionClaims, userId, orgId, actor, orgRole, orgSlug, orgPermissions } = derivedState.value; + return { sessionId, sessionClaims, userId, actor, orgId, orgRole, orgSlug, orgPermissions }; }); const clientCtx = computed(() => resources.value.client); const userCtx = computed(() => derivedState.value.user); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 213c909b658..beb66959411 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -6,6 +6,7 @@ import type { ClientResource, CustomMenuItem, CustomPage, + JwtPayload, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, @@ -19,6 +20,7 @@ export interface VueClerkInjectionKeyType { authCtx: ComputedRef<{ userId: string | null | undefined; sessionId: string | null | undefined; + sessionClaims: JwtPayload | null | undefined; actor: ActJWTClaim | null | undefined; orgId: string | null | undefined; orgRole: OrganizationCustomRoleKey | null | undefined;