Skip to content

Commit 8709ce8

Browse files
feat(nextjs): expose organization, isInitialized, clientId, signInOptions, switchOrganization and getDecodedIdToken in useAsgardeo()
The Next.js context lacked several fields the React SDK's context provides; the bundled sample already read `organization` and got undefined. - organization: the current organization from the server provider. - isInitialized: true (the client provider is only rendered once the client has been initialized), clientId and signInOptions from the configuration. - switchOrganization(): the server action, followed by router.refresh() so the server components pick up the switched session. - getDecodedIdToken(): the ID token claims through a new server action; the raw tokens stay in the HttpOnly cookie (hence no getAccessToken/getIdToken/ exchangeToken in the browser). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 409ebae commit 8709ce8

6 files changed

Lines changed: 215 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@asgardeo/nextjs': patch
3+
---
4+
5+
`useAsgardeo()` exposes more of what the React SDK's context provides: `organization` (the current organization, which the bundled sample already reads), `isInitialized`, `clientId`, `signInOptions`, `switchOrganization()` (which re-renders the server components once the switch has happened) and `getDecodedIdToken()` (the ID token claims, resolved through a server action so the tokens themselves stay in the HttpOnly cookie).

‎packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,36 @@
1818

1919
'use client';
2020

21+
import {IdToken, Organization, TokenResponse} from '@asgardeo/node';
2122
import {AsgardeoContextProps as AsgardeoReactContextProps} from '@asgardeo/react';
2223
import {Context, createContext} from 'react';
2324
import {RefreshResult} from '../../../server/actions/refreshToken';
2425

2526
/**
2627
* Props interface of {@link AsgardeoContext}
28+
*
29+
* A subset of the React SDK's context: the raw tokens never reach the browser (they live in the HttpOnly
30+
* session cookie), so `getAccessToken`, `getIdToken` and `exchangeToken` are not available here. Use
31+
* `http.request` for authenticated calls and `getDecodedIdToken` for the ID token claims.
2732
*/
28-
export type AsgardeoContextProps = Partial<AsgardeoReactContextProps> & {
33+
export type AsgardeoContextProps = Partial<
34+
Omit<AsgardeoReactContextProps, 'getDecodedIdToken' | 'switchOrganization' | 'organization'>
35+
> & {
2936
clearSession?: () => Promise<void>;
37+
/**
38+
* Returns the decoded ID token (its claims) of the signed-in user, resolved through a server action.
39+
*/
40+
getDecodedIdToken?: () => Promise<IdToken>;
41+
/**
42+
* The organization the session belongs to, or `null` while signed out or unknown.
43+
*/
44+
organization?: Organization | null;
3045
refreshToken?: () => Promise<RefreshResult>;
46+
/**
47+
* Switches the session to `organization` and re-renders the server components so the new organization,
48+
* user and organization list are picked up.
49+
*/
50+
switchOrganization?: (organization: Organization) => Promise<TokenResponse | Response>;
3151
};
3252

3353
/**
@@ -38,16 +58,21 @@ const AsgardeoContext: Context<AsgardeoContextProps | null> = createContext<null
3858
applicationId: undefined,
3959
baseUrl: undefined,
4060
clearSession: () => Promise.resolve(),
61+
clientId: undefined,
62+
getDecodedIdToken: () => Promise.resolve({} as IdToken),
4163
isInitialized: false,
4264
isLoading: true,
4365
isSignedIn: false,
66+
organization: null,
4467
organizationHandle: undefined,
4568
refreshToken: () => Promise.resolve({expiresAt: 0}),
4669
signIn: () => Promise.resolve({} as any),
70+
signInOptions: {},
4771
signInUrl: undefined,
4872
signOut: () => Promise.resolve({} as any),
4973
signUp: () => Promise.resolve({} as any),
5074
signUpUrl: undefined,
75+
switchOrganization: () => Promise.resolve({} as TokenResponse),
5176
user: null,
5277
});
5378

‎packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx‎

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
EmbeddedFlowStatus,
3636
HttpRequestConfig,
3737
HttpResponse,
38+
IdToken,
3839
} from '@asgardeo/node';
3940
import {
4041
I18nProvider,
@@ -66,8 +67,14 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
6667
brandingPreference?: BrandingPreference | null;
6768
clearSession: () => Promise<void>;
6869
createOrganization: (payload: CreateOrganizationPayload, sessionId: string) => Promise<Organization>;
69-
currentOrganization: Organization;
70+
currentOrganization: Organization | null;
7071
getAllOrganizations: (options?: any, sessionId?: string) => Promise<AllOrganizationsApiResponse>;
72+
/**
73+
* Server action returning the decoded ID token of the signed-in user.
74+
*/
75+
getDecodedIdToken?: (
76+
sessionId?: string,
77+
) => Promise<{data: {idToken?: IdToken}; error: string | null; success: boolean}>;
7178
handleOAuthCallback: (
7279
code: string,
7380
state: string,
@@ -118,6 +125,9 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
118125
brandingPreference,
119126
afterSignInUrl,
120127
httpRequest,
128+
getDecodedIdToken,
129+
signInOptions,
130+
clientId,
121131
}: PropsWithChildren<AsgardeoClientProviderProps>) => {
122132
const reRenderCheckRef: RefObject<boolean> = useRef(false);
123133
const router: AppRouterInstance = useRouter();
@@ -353,25 +363,69 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
353363
const handleHttpRequestAll = async (requestConfigs?: HttpRequestConfig[]): Promise<HttpResponse[]> =>
354364
Promise.all((requestConfigs ?? []).map((requestConfig: HttpRequestConfig) => handleHttpRequest(requestConfig)));
355365

366+
/**
367+
* Returns the decoded ID token through the server action; the raw token stays in the HttpOnly cookie.
368+
*/
369+
const handleGetDecodedIdToken = async (): Promise<IdToken> => {
370+
if (!getDecodedIdToken) {
371+
throw new AsgardeoRuntimeError(
372+
'`getDecodedIdToken` is not available. Make sure the component is rendered inside `<AsgardeoProvider>`.',
373+
'AsgardeoClientProvider-handleGetDecodedIdToken-RuntimeError-001',
374+
'nextjs',
375+
);
376+
}
377+
378+
const result: {data: {idToken?: IdToken}; error: string | null; success: boolean} = await getDecodedIdToken();
379+
380+
if (!result.success || !result.data.idToken) {
381+
throw new AsgardeoRuntimeError(
382+
result.error ?? 'Failed to get the decoded ID token.',
383+
'AsgardeoClientProvider-handleGetDecodedIdToken-RuntimeError-002',
384+
'nextjs',
385+
);
386+
}
387+
388+
return result.data.idToken;
389+
};
390+
391+
/**
392+
* Switches the session to `organization` and re-renders the server components, which re-read the session
393+
* cookie and hand the new organization, user and organization list down.
394+
*/
395+
const handleSwitchOrganization = async (organization: Organization): Promise<TokenResponse | Response> => {
396+
const response: TokenResponse | Response = await switchOrganization(organization);
397+
398+
router.refresh();
399+
400+
return response;
401+
};
402+
356403
const contextValue: AsgardeoContextProps = useMemo(
357404
() => ({
358405
afterSignInUrl,
359406
applicationId,
360407
baseUrl,
361408
clearSession,
409+
clientId,
410+
getDecodedIdToken: handleGetDecodedIdToken,
362411
http: {
363412
request: handleHttpRequest,
364413
requestAll: handleHttpRequestAll,
365414
},
415+
// The server provider only renders this provider once the client has been initialized.
416+
isInitialized: true,
366417
isLoading,
367418
isSignedIn,
419+
organization: currentOrganization,
368420
organizationHandle,
369421
refreshToken,
370422
signIn: handleSignIn,
423+
signInOptions: signInOptions ?? {},
371424
signInUrl,
372425
signOut: handleSignOut,
373426
signUp: handleSignUp,
374427
signUpUrl,
428+
switchOrganization: handleSwitchOrganization,
375429
user,
376430
}),
377431
[
@@ -385,6 +439,11 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
385439
organizationHandle,
386440
afterSignInUrl,
387441
httpRequest,
442+
clientId,
443+
currentOrganization,
444+
signInOptions,
445+
getDecodedIdToken,
446+
switchOrganization,
388447
],
389448
);
390449

@@ -413,7 +472,7 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
413472
getAllOrganizations={getAllOrganizations}
414473
myOrganizations={myOrganizations}
415474
currentOrganization={currentOrganization}
416-
onOrganizationSwitch={switchOrganization as any}
475+
onOrganizationSwitch={handleSwitchOrganization}
417476
revalidateMyOrganizations={revalidateMyOrganizations as any}
418477
>
419478
{children}

‎packages/nextjs/src/server/AsgardeoProvider.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import createOrganization from './actions/createOrganization';
2626
import getAllOrganizations from './actions/getAllOrganizations';
2727
import getBrandingPreference from './actions/getBrandingPreference';
2828
import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction';
29+
import getDecodedIdTokenAction from './actions/getDecodedIdTokenAction';
2930
import getMyOrganizations from './actions/getMyOrganizations';
3031
import getSessionId from './actions/getSessionId';
3132
import getSessionPayload from './actions/getSessionPayload';
@@ -227,6 +228,8 @@ const AsgardeoServerProvider: FC<PropsWithChildren<AsgardeoServerProviderProps>>
227228
signUpUrl={config?.signUpUrl}
228229
afterSignInUrl={config?.afterSignInUrl}
229230
httpRequest={httpRequestAction}
231+
getDecodedIdToken={getDecodedIdTokenAction}
232+
signInOptions={config?.signInOptions}
230233
preferences={config?.preferences}
231234
clientId={config?.clientId}
232235
user={user}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {beforeEach, describe, expect, it, vi, Mock} from 'vitest';
20+
import AsgardeoNextClient from '../../../AsgardeoNextClient';
21+
import getDecodedIdTokenAction from '../getDecodedIdTokenAction';
22+
import getSessionId from '../getSessionId';
23+
24+
vi.mock('../../../AsgardeoNextClient', () => ({
25+
default: {
26+
getInstance: vi.fn(),
27+
},
28+
}));
29+
30+
vi.mock('../getSessionId', () => ({
31+
default: vi.fn(async () => 'session-from-cookie'),
32+
}));
33+
34+
describe('getDecodedIdTokenAction', () => {
35+
type ActionResult = Awaited<ReturnType<typeof getDecodedIdTokenAction>>;
36+
37+
const client: {getDecodedIdToken: Mock} = {getDecodedIdToken: vi.fn()};
38+
const idToken: Record<string, unknown> = {aud: 'client-id', email: 'jane@example.com', iss: 'issuer', sub: 'user-1'};
39+
40+
beforeEach(() => {
41+
vi.clearAllMocks();
42+
(AsgardeoNextClient.getInstance as unknown as Mock).mockReturnValue(client);
43+
(getSessionId as unknown as Mock).mockResolvedValue('session-from-cookie');
44+
});
45+
46+
it('returns the decoded ID token for the given session', async () => {
47+
client.getDecodedIdToken.mockResolvedValue(idToken);
48+
49+
const result: ActionResult = await getDecodedIdTokenAction('session-1');
50+
51+
expect(client.getDecodedIdToken).toHaveBeenCalledWith('session-1');
52+
expect(result).toEqual({data: {idToken}, error: null, success: true});
53+
});
54+
55+
it('resolves the session from the cookie when no session ID is given', async () => {
56+
client.getDecodedIdToken.mockResolvedValue(idToken);
57+
58+
await getDecodedIdTokenAction();
59+
60+
expect(client.getDecodedIdToken).toHaveBeenCalledWith('session-from-cookie');
61+
});
62+
63+
it('reports the failure reason instead of throwing', async () => {
64+
client.getDecodedIdToken.mockRejectedValue(new Error('No session'));
65+
66+
const result: ActionResult = await getDecodedIdTokenAction();
67+
68+
expect(result).toEqual({data: {}, error: 'No session', success: false});
69+
});
70+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
'use server';
20+
21+
import {IdToken} from '@asgardeo/node';
22+
import getSessionId from './getSessionId';
23+
import AsgardeoNextClient from '../../AsgardeoNextClient';
24+
25+
/**
26+
* Server action that returns the decoded ID token (its claims) of the signed-in user.
27+
*
28+
* Only the decoded claims cross the server boundary; the raw tokens stay in the HttpOnly session cookie.
29+
* Backs `useAsgardeo().getDecodedIdToken()` in Client Components.
30+
*
31+
* @param sessionId - Optional session ID; resolved from the session cookie when omitted.
32+
*/
33+
const getDecodedIdTokenAction = async (
34+
sessionId?: string,
35+
): Promise<{data: {idToken?: IdToken}; error: string | null; success: boolean}> => {
36+
try {
37+
const client: AsgardeoNextClient = AsgardeoNextClient.getInstance();
38+
const idToken: IdToken = await client.getDecodedIdToken(sessionId ?? (await getSessionId()));
39+
40+
return {data: {idToken}, error: null, success: true};
41+
} catch (error) {
42+
return {
43+
data: {},
44+
error: error instanceof Error ? error.message : String(error),
45+
success: false,
46+
};
47+
}
48+
};
49+
50+
export default getDecodedIdTokenAction;

0 commit comments

Comments
 (0)