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
14 changes: 14 additions & 0 deletions packages/javascript/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,17 @@ await esbuild.build({
outfile: 'dist/cjs/index.js',
sourcemap: true,
});

await esbuild.build({
...commonOptions,
define: {
// process.versions is a Node.js-only API not available in the Edge Runtime.
// Replacing it with undefined makes isNode() evaluate to false, causing the
// logger to fall back to plain console.log() — the correct behaviour for Edge.
'process.versions': 'undefined',
},
format: 'esm',
outfile: 'dist/edge/index.js',
platform: 'browser',
sourcemap: true,
});
1 change: 1 addition & 0 deletions packages/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"main": "dist/cjs/index.js",
"module": "dist/index.js",
"exports": {
"edge-light": "./dist/edge/index.js",
"import": "./dist/index.js",
"require": "./dist/cjs/index.js"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {build} from 'esbuild';

const commonOptions = {
bundle: false,
entryPoints: ['src/index.ts', 'src/server/index.ts'],
entryPoints: ['src/index.ts', 'src/server/index.ts', 'src/middleware.ts'],
platform: 'node',
target: ['node18'],
};
Expand Down
6 changes: 6 additions & 0 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
"types": "./dist/types/server/index.d.ts",
"import": "./dist/esm/server/index.js",
"require": "./dist/cjs/server/index.js"
},
"./middleware": {
"types": "./dist/types/middleware.d.ts",
"edge-light": "./dist/esm/middleware.js",
"import": "./dist/esm/middleware.js",
"require": "./dist/cjs/middleware.js"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@

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

/**
* Props interface of {@link AsgardeoContext}
*/
export type AsgardeoContextProps = Partial<AsgardeoReactContextProps>;
export type AsgardeoContextProps = Partial<AsgardeoReactContextProps> & {
clearSession?: () => Promise<void>;
refreshToken?: () => Promise<RefreshResult>;
};

/**
* Context object for managing the Authentication flow builder core context.
Expand All @@ -33,10 +37,12 @@ const AsgardeoContext: Context<AsgardeoContextProps | null> = createContext<null
afterSignInUrl: undefined,
applicationId: undefined,
baseUrl: undefined,
clearSession: () => Promise.resolve(),
isInitialized: false,
isLoading: true,
isSignedIn: false,
organizationHandle: undefined,
refreshToken: () => Promise.resolve({expiresAt: 0}),
signIn: () => Promise.resolve({} as any),
signInUrl: undefined,
signOut: () => Promise.resolve({} as any),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-
import {useRouter, useSearchParams} from 'next/navigation';
import {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';
import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext';
import {RefreshResult} from '../../../server/actions/refreshToken';
import logger from '../../../utils/logger';

/**
Expand All @@ -57,6 +58,7 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
Pick<AsgardeoProviderProps, 'baseUrl' | 'clientId'> & {
applicationId: AsgardeoContextProps['applicationId'];
brandingPreference?: BrandingPreference | null;
clearSession: () => Promise<void>;
createOrganization: (payload: CreateOrganizationPayload, sessionId: string) => Promise<Organization>;
currentOrganization: Organization;
getAllOrganizations: (options?: any, sessionId?: string) => Promise<AllOrganizationsApiResponse>;
Expand All @@ -68,6 +70,7 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
isSignedIn: boolean;
myOrganizations: Organization[];
organizationHandle: AsgardeoContextProps['organizationHandle'];
refreshToken: () => Promise<RefreshResult>;
revalidateMyOrganizations?: (sessionId?: string) => Promise<Organization[]>;
signIn: AsgardeoContextProps['signIn'];
signOut: AsgardeoContextProps['signOut'];
Expand All @@ -85,6 +88,8 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
baseUrl,
children,
signIn,
clearSession,
refreshToken,
signOut,
signUp,
handleOAuthCallback,
Expand Down Expand Up @@ -292,9 +297,11 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
() => ({
applicationId,
baseUrl,
clearSession,
isLoading,
isSignedIn,
organizationHandle,
refreshToken,
signIn: handleSignIn,
signInUrl,
signOut: handleSignOut,
Expand Down
50 changes: 50 additions & 0 deletions packages/nextjs/src/constants/sessionConstants.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.
*/

/**
* Number of seconds before access token expiry at which the SDK proactively
* refreshes the token. A 25-second buffer prevents races where the token is
* valid when a request starts but expires mid-flight.
*/
export const REFRESH_BUFFER_SECONDS: number = 25;

/**
* Default session cookie lifetime in seconds (24 hours).
*
* Used when no explicit session cookie expiry is configured. The session cookie
* lifetime can be overridden in two ways (evaluated in this order):
*
* 1. `sessionCookieExpiryTime` in `AsgardeoNodeConfig` — set programmatically
* when initialising the SDK.
* 2. `ASGARDEO_SESSION_COOKIE_EXPIRY_TIME` environment variable — set in `.env`
* (e.g. `ASGARDEO_SESSION_COOKIE_EXPIRY_TIME=86400`).
* 3. This constant — applied when neither of the above is present.
*
* Two independent expiry bounds apply to the session and they are generally
* NOT the same value:
*
* - JWT `exp` claim — set by `SessionManager.createSessionToken(...)` from
* the `accessTokenTtlSeconds` argument (i.e. the access token's `expires_in`
* returned by the auth server, typically ~1 hour). This controls when
* `verifySessionToken` rejects the token and is the trigger for a refresh.
* - Browser cookie `maxAge` — set by the caller (sign-in / refresh / org-switch
* actions) from `SessionManager.resolveSessionCookieExpiry(...)`, which returns
* this constant by default (24 hours). This controls how long the browser
* holds the cookie before discarding it.
*/
export const DEFAULT_SESSION_COOKIE_EXPIRY_TIME: number = 86400;
37 changes: 37 additions & 0 deletions packages/nextjs/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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.
*/

/**
* Edge Runtime entry point — safe for use in Next.js middleware.ts.
*
* This file must only import modules whose full transitive dependency graph
* contains zero Node.js-only APIs (process.versions, fs, crypto, etc.).
* Permitted dependencies: jose, fetch, next/server, and local utilities
* that themselves satisfy the same constraint.
*
* Do NOT import from:
* - AsgardeoNextClient (depends on @asgardeo/node → @asgardeo/javascript)
* - server/AsgardeoProvider (depends on @asgardeo/node)
* - server/actions/* (depend on @asgardeo/node)
* - client/* (depend on @asgardeo/javascript via @asgardeo/react)
*/

export {default as asgardeoMiddleware} from './server/middleware/asgardeoMiddleware';
export * from './server/middleware/asgardeoMiddleware';

export {default as createRouteMatcher} from './server/middleware/createRouteMatcher';
20 changes: 19 additions & 1 deletion packages/nextjs/src/server/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import {BrandingPreference, AsgardeoRuntimeError, IdToken, Organization, User, UserProfile} from '@asgardeo/node';
import {AsgardeoProviderProps} from '@asgardeo/react';
import {FC, PropsWithChildren, ReactElement} from 'react';
import clearSession from './actions/clearSession';
import createOrganization from './actions/createOrganization';
import getAllOrganizations from './actions/getAllOrganizations';
import getBrandingPreference from './actions/getBrandingPreference';
Expand All @@ -32,6 +33,7 @@ import getUserAction from './actions/getUserAction';
import getUserProfileAction from './actions/getUserProfileAction';
import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction';
import isSignedIn from './actions/isSignedIn';
import refreshToken from './actions/refreshToken';
import signInAction from './actions/signInAction';
import signOutAction from './actions/signOutAction';
import signUpAction from './actions/signUpAction';
Expand All @@ -48,6 +50,20 @@ import {SessionTokenPayload} from '../utils/SessionManager';
*/
export type AsgardeoServerProviderProps = Partial<AsgardeoProviderProps> & {
clientSecret?: string;
/**
* Session cookie lifetime in seconds. Determines how long the session cookie
* remains valid in the browser after sign-in.
*
* Resolution order (first defined value wins):
* 1. This prop — set here when mounting the provider.
* 2. `ASGARDEO_SESSION_COOKIE_EXPIRY_TIME` environment variable.
* 3. Built-in default of 86400 seconds (24 hours).
*
* @example
* // 8-hour session cookie
* <AsgardeoServerProvider sessionCookieExpiryTime={28800} ... />
*/
sessionCookieExpiryTime?: number;
};

/**
Expand Down Expand Up @@ -99,7 +115,7 @@ const AsgardeoServerProvider: FC<PropsWithChildren<AsgardeoServerProviderProps>>
// Try to get session information from JWT first, then fall back to legacy
const sessionPayload: SessionTokenPayload | undefined = await getSessionPayload();
const sessionId: string = sessionPayload?.sessionId || (await getSessionId()) || '';
const signedIn: boolean = sessionPayload ? true : await isSignedIn(sessionId);
const signedIn: boolean = await isSignedIn(sessionId);

let user: User = {};
let userProfile: UserProfile = {
Expand Down Expand Up @@ -203,6 +219,8 @@ const AsgardeoServerProvider: FC<PropsWithChildren<AsgardeoServerProviderProps>>
applicationId={config?.applicationId}
baseUrl={config?.baseUrl}
signIn={signInAction}
clearSession={clearSession}
refreshToken={refreshToken}
signOut={signOutAction}
signUp={signUpAction}
handleOAuthCallback={handleOAuthCallbackAction}
Expand Down
54 changes: 54 additions & 0 deletions packages/nextjs/src/server/actions/clearSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* 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 {cookies} from 'next/headers';
import logger from '../../utils/logger';
import SessionManager from '../../utils/SessionManager';

type RequestCookies = Awaited<ReturnType<typeof cookies>>;

/**
* Deletes all Asgardeo session cookies from the browser without contacting the
* identity server.
*
* Use this for error-recovery scenarios where the local session must be wiped
* immediately: refresh token failures, corrupt sessions, or forced local sign-out
* when the identity server is unreachable.
*
* For a complete sign-out that also revokes the server-side session and obtains the
* after-sign-out redirect URL, use `signOutAction` instead.
*
* @example
* ```typescript
* import { clearSession } from '@asgardeo/nextjs/server';
*
* // Inside a Server Action or Route Handler:
* await clearSession();
* redirect('/sign-in');
* ```
*/
const clearSession = async (): Promise<void> => {
const cookieStore: RequestCookies = await cookies();
cookieStore.delete(SessionManager.getSessionCookieName());
cookieStore.delete(SessionManager.getTempSessionCookieName());
logger.debug('[clearSession] Session cookies cleared.');
};

export default clearSession;
5 changes: 3 additions & 2 deletions packages/nextjs/src/server/actions/getAccessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@

'use server';

import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';
import {cookies} from 'next/headers';
import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';

type RequestCookies = Awaited<ReturnType<typeof cookies>>;

/**
* Get the access token from the session cookie.
*
* @returns The access token if it exists, undefined otherwise
*/
const getAccessToken = async (): Promise<string | undefined> => {
const cookieStore: ReadonlyRequestCookies = await cookies();
const cookieStore: RequestCookies = await cookies();

const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value;

Expand Down
5 changes: 3 additions & 2 deletions packages/nextjs/src/server/actions/getSessionId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@

'use server';

import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';
import {cookies} from 'next/headers';
import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';

type RequestCookies = Awaited<ReturnType<typeof cookies>>;

/**
* Get the session ID from cookies.
* Tries JWT session first, then falls back to legacy session ID.
*
* @returns The session ID if it exists, undefined otherwise
*/
const getSessionId = async (): Promise<string | undefined> => {
const cookieStore: ReadonlyRequestCookies = await cookies();
const cookieStore: RequestCookies = await cookies();

const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value;

Expand Down
5 changes: 3 additions & 2 deletions packages/nextjs/src/server/actions/getSessionPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@

'use server';

import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';
import {cookies} from 'next/headers';
import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';

type RequestCookies = Awaited<ReturnType<typeof cookies>>;

/**
* Get the session payload from JWT session cookie.
* This includes user ID, session ID, scopes, and organization ID.
*
* @returns The session payload if valid JWT session exists, undefined otherwise
*/
const getSessionPayload = async (): Promise<SessionTokenPayload | undefined> => {
const cookieStore: ReadonlyRequestCookies = await cookies();
const cookieStore: RequestCookies = await cookies();

const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value;
if (!sessionToken) {
Expand Down
Loading
Loading