Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/nextjs-context-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/nextjs': patch
---

`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).
27 changes: 26 additions & 1 deletion packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,36 @@

'use client';

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

/**
* Props interface of {@link AsgardeoContext}
*
* A subset of the React SDK's context: the raw tokens never reach the browser (they live in the HttpOnly
* session cookie), so `getAccessToken`, `getIdToken` and `exchangeToken` are not available here. Use
* `http.request` for authenticated calls and `getDecodedIdToken` for the ID token claims.
*/
export type AsgardeoContextProps = Partial<AsgardeoReactContextProps> & {
export type AsgardeoContextProps = Partial<
Omit<AsgardeoReactContextProps, 'getDecodedIdToken' | 'switchOrganization' | 'organization'>
> & {
clearSession?: () => Promise<void>;
/**
* Returns the decoded ID token (its claims) of the signed-in user, resolved through a server action.
*/
getDecodedIdToken?: () => Promise<IdToken>;
/**
* The organization the session belongs to, or `null` while signed out or unknown.
*/
organization?: Organization | null;
refreshToken?: () => Promise<RefreshResult>;
/**
* Switches the session to `organization` and re-renders the server components so the new organization,
* user and organization list are picked up.
*/
switchOrganization?: (organization: Organization) => Promise<TokenResponse | Response>;
};

/**
Expand All @@ -38,16 +58,21 @@ const AsgardeoContext: Context<AsgardeoContextProps | null> = createContext<null
applicationId: undefined,
baseUrl: undefined,
clearSession: () => Promise.resolve(),
clientId: undefined,
getDecodedIdToken: () => Promise.resolve({} as IdToken),
isInitialized: false,
isLoading: true,
isSignedIn: false,
organization: null,
organizationHandle: undefined,
refreshToken: () => Promise.resolve({expiresAt: 0}),
signIn: () => Promise.resolve({} as any),
signInOptions: {},
signInUrl: undefined,
signOut: () => Promise.resolve({} as any),
signUp: () => Promise.resolve({} as any),
signUpUrl: undefined,
switchOrganization: () => Promise.resolve({} as TokenResponse),
user: null,
});

Expand Down
63 changes: 61 additions & 2 deletions packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
EmbeddedFlowStatus,
HttpRequestConfig,
HttpResponse,
IdToken,
} from '@asgardeo/node';
import {
I18nProvider,
Expand Down Expand Up @@ -66,8 +67,14 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
brandingPreference?: BrandingPreference | null;
clearSession: () => Promise<void>;
createOrganization: (payload: CreateOrganizationPayload, sessionId: string) => Promise<Organization>;
currentOrganization: Organization;
currentOrganization: Organization | null;
getAllOrganizations: (options?: any, sessionId?: string) => Promise<AllOrganizationsApiResponse>;
/**
* Server action returning the decoded ID token of the signed-in user.
*/
getDecodedIdToken?: (
sessionId?: string,
) => Promise<{data: {idToken?: IdToken}; error: string | null; success: boolean}>;
handleOAuthCallback: (
code: string,
state: string,
Expand Down Expand Up @@ -118,6 +125,9 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
brandingPreference,
afterSignInUrl,
httpRequest,
getDecodedIdToken,
signInOptions,
clientId,
}: PropsWithChildren<AsgardeoClientProviderProps>) => {
const reRenderCheckRef: RefObject<boolean> = useRef(false);
const router: AppRouterInstance = useRouter();
Expand Down Expand Up @@ -353,25 +363,69 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
const handleHttpRequestAll = async (requestConfigs?: HttpRequestConfig[]): Promise<HttpResponse[]> =>
Promise.all((requestConfigs ?? []).map((requestConfig: HttpRequestConfig) => handleHttpRequest(requestConfig)));

/**
* Returns the decoded ID token through the server action; the raw token stays in the HttpOnly cookie.
*/
const handleGetDecodedIdToken = async (): Promise<IdToken> => {
if (!getDecodedIdToken) {
throw new AsgardeoRuntimeError(
'`getDecodedIdToken` is not available. Make sure the component is rendered inside `<AsgardeoProvider>`.',
'AsgardeoClientProvider-handleGetDecodedIdToken-RuntimeError-001',
'nextjs',
);
}

const result: {data: {idToken?: IdToken}; error: string | null; success: boolean} = await getDecodedIdToken();

if (!result.success || !result.data.idToken) {
throw new AsgardeoRuntimeError(
result.error ?? 'Failed to get the decoded ID token.',
'AsgardeoClientProvider-handleGetDecodedIdToken-RuntimeError-002',
'nextjs',
);
}

return result.data.idToken;
};

/**
* Switches the session to `organization` and re-renders the server components, which re-read the session
* cookie and hand the new organization, user and organization list down.
*/
const handleSwitchOrganization = async (organization: Organization): Promise<TokenResponse | Response> => {
const response: TokenResponse | Response = await switchOrganization(organization);

router.refresh();

return response;
};

const contextValue: AsgardeoContextProps = useMemo(
() => ({
afterSignInUrl,
applicationId,
baseUrl,
clearSession,
clientId,
getDecodedIdToken: handleGetDecodedIdToken,
http: {
request: handleHttpRequest,
requestAll: handleHttpRequestAll,
},
// The server provider only renders this provider once the client has been initialized.
isInitialized: true,
isLoading,
isSignedIn,
organization: currentOrganization,
organizationHandle,
refreshToken,
signIn: handleSignIn,
signInOptions: signInOptions ?? {},
signInUrl,
signOut: handleSignOut,
signUp: handleSignUp,
signUpUrl,
switchOrganization: handleSwitchOrganization,
user,
}),
[
Expand All @@ -385,6 +439,11 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
organizationHandle,
afterSignInUrl,
httpRequest,
clientId,
currentOrganization,
signInOptions,
getDecodedIdToken,
switchOrganization,
],
);

Expand Down Expand Up @@ -413,7 +472,7 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
getAllOrganizations={getAllOrganizations}
myOrganizations={myOrganizations}
currentOrganization={currentOrganization}
onOrganizationSwitch={switchOrganization as any}
onOrganizationSwitch={handleSwitchOrganization}
revalidateMyOrganizations={revalidateMyOrganizations as any}
>
{children}
Expand Down
3 changes: 3 additions & 0 deletions packages/nextjs/src/server/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import createOrganization from './actions/createOrganization';
import getAllOrganizations from './actions/getAllOrganizations';
import getBrandingPreference from './actions/getBrandingPreference';
import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction';
import getDecodedIdTokenAction from './actions/getDecodedIdTokenAction';
import getMyOrganizations from './actions/getMyOrganizations';
import getSessionId from './actions/getSessionId';
import getSessionPayload from './actions/getSessionPayload';
Expand Down Expand Up @@ -227,6 +228,8 @@ const AsgardeoServerProvider: FC<PropsWithChildren<AsgardeoServerProviderProps>>
signUpUrl={config?.signUpUrl}
afterSignInUrl={config?.afterSignInUrl}
httpRequest={httpRequestAction}
getDecodedIdToken={getDecodedIdTokenAction}
signInOptions={config?.signInOptions}
preferences={config?.preferences}
clientId={config?.clientId}
user={user}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {beforeEach, describe, expect, it, vi, Mock} from 'vitest';
import AsgardeoNextClient from '../../../AsgardeoNextClient';
import getDecodedIdTokenAction from '../getDecodedIdTokenAction';
import getSessionId from '../getSessionId';

vi.mock('../../../AsgardeoNextClient', () => ({
default: {
getInstance: vi.fn(),
},
}));

vi.mock('../getSessionId', () => ({
default: vi.fn(async () => 'session-from-cookie'),
}));

describe('getDecodedIdTokenAction', () => {
type ActionResult = Awaited<ReturnType<typeof getDecodedIdTokenAction>>;

const client: {getDecodedIdToken: Mock} = {getDecodedIdToken: vi.fn()};
const idToken: Record<string, unknown> = {aud: 'client-id', email: 'jane@example.com', iss: 'issuer', sub: 'user-1'};

beforeEach(() => {
vi.clearAllMocks();
(AsgardeoNextClient.getInstance as unknown as Mock).mockReturnValue(client);
(getSessionId as unknown as Mock).mockResolvedValue('session-from-cookie');
});

it('returns the decoded ID token for the given session', async () => {
client.getDecodedIdToken.mockResolvedValue(idToken);

const result: ActionResult = await getDecodedIdTokenAction('session-1');

expect(client.getDecodedIdToken).toHaveBeenCalledWith('session-1');
expect(result).toEqual({data: {idToken}, error: null, success: true});
});

it('resolves the session from the cookie when no session ID is given', async () => {
client.getDecodedIdToken.mockResolvedValue(idToken);

await getDecodedIdTokenAction();

expect(client.getDecodedIdToken).toHaveBeenCalledWith('session-from-cookie');
});

it('reports the failure reason instead of throwing', async () => {
client.getDecodedIdToken.mockRejectedValue(new Error('No session'));

const result: ActionResult = await getDecodedIdTokenAction();

expect(result).toEqual({data: {}, error: 'No session', success: false});
});
});
50 changes: 50 additions & 0 deletions packages/nextjs/src/server/actions/getDecodedIdTokenAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

'use server';

import {IdToken} from '@asgardeo/node';
import getSessionId from './getSessionId';
import AsgardeoNextClient from '../../AsgardeoNextClient';

/**
* Server action that returns the decoded ID token (its claims) of the signed-in user.
*
* Only the decoded claims cross the server boundary; the raw tokens stay in the HttpOnly session cookie.
* Backs `useAsgardeo().getDecodedIdToken()` in Client Components.
*
* @param sessionId - Optional session ID; resolved from the session cookie when omitted.
*/
const getDecodedIdTokenAction = async (
sessionId?: string,
): Promise<{data: {idToken?: IdToken}; error: string | null; success: boolean}> => {
try {
const client: AsgardeoNextClient = AsgardeoNextClient.getInstance();
const idToken: IdToken = await client.getDecodedIdToken(sessionId ?? (await getSessionId()));

return {data: {idToken}, error: null, success: true};
} catch (error) {
return {
data: {},
error: error instanceof Error ? error.message : String(error),
success: false,
};
}
};

export default getDecodedIdTokenAction;
Loading