diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 1f7e16277..179540ee8 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -66,31 +66,54 @@ import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; * Client for mplementing Asgardeo in Next.js applications. * This class provides the core functionality for managing user authentication and sessions. * - * This class is implemented as a singleton to ensure a single instance across the application. + * This class is implemented as a multiton to support multiple independent instances across the application. * * @typeParam T - Configuration type that extends AsgardeoNextConfig. */ class AsgardeoNextClient extends AsgardeoNodeClient { - private static instance: AsgardeoNextClient; + private static instances: Map> = new Map(); + + private instanceId: number; private asgardeo: LegacyAsgardeoNodeClient; public isInitialized: boolean = false; - private constructor() { + private constructor(instanceId: number = 0) { super(); - + this.instanceId = instanceId; this.asgardeo = new LegacyAsgardeoNodeClient(); } /** - * Get the singleton instance of AsgardeoNextClient + * Get the instance of AsgardeoNextClient for the given instanceId. */ - public static getInstance(): AsgardeoNextClient { - if (!AsgardeoNextClient.instance) { - AsgardeoNextClient.instance = new AsgardeoNextClient(); + public static getInstance(instanceId: number = 0): AsgardeoNextClient { + if (!AsgardeoNextClient.instances.has(instanceId)) { + AsgardeoNextClient.instances.set(instanceId, new AsgardeoNextClient(instanceId)); } - return AsgardeoNextClient.instance as AsgardeoNextClient; + return AsgardeoNextClient.instances.get(instanceId) as AsgardeoNextClient; + } + + /** + * Returns the instanceId of this client instance. + */ + public getInstanceId(): number { + return this.instanceId; + } + + /** + * Returns whether an instance with the given instanceId exists. + */ + public static hasInstance(instanceId: number = 0): boolean { + return AsgardeoNextClient.instances.has(instanceId); + } + + /** + * Destroys the instance with the given instanceId. + */ + public static destroyInstance(instanceId: number = 0): boolean { + return AsgardeoNextClient.instances.delete(instanceId); } /** @@ -146,6 +169,7 @@ class AsgardeoNextClient exte ...rest, } as any, storage, + this.instanceId, ); } diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 475192363..ea5026f7e 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -104,6 +104,7 @@ const AsgardeoClientProvider: FC> getAllOrganizations, switchOrganization, brandingPreference, + instanceId = 0, }: PropsWithChildren) => { const reRenderCheckRef: RefObject = useRef(false); const router: AppRouterInstance = useRouter(); @@ -151,8 +152,11 @@ const AsgardeoClientProvider: FC> return; } + // Check for what instance the callback is for + const callbackInstanceId: string | null = state ? state.split('_')[1] : null; + // Handle OAuth callback if code and state are present - if (code && state) { + if (code && state && callbackInstanceId === instanceId.toString()) { setIsLoading(true); const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback( diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index d4760f7b4..65bdbc3e2 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -18,7 +18,14 @@ 'use server'; -import {BrandingPreference, AsgardeoRuntimeError, IdToken, Organization, User, UserProfile} from '@asgardeo/node'; +import { + BrandingPreference, + AsgardeoRuntimeError, + IdToken, + Organization, + User, + UserProfile, +} from '@asgardeo/node'; import {AsgardeoProviderProps} from '@asgardeo/react'; import {FC, PropsWithChildren, ReactElement} from 'react'; import createOrganization from './actions/createOrganization'; @@ -70,9 +77,10 @@ const AsgardeoServerProvider: FC> children, afterSignInUrl, afterSignOutUrl, + instanceId = 0, ..._config }: PropsWithChildren): Promise => { - const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); let config: Partial = {}; try { @@ -97,9 +105,9 @@ const AsgardeoServerProvider: FC> } // 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 sessionPayload: SessionTokenPayload | undefined = await getSessionPayload(instanceId); + const sessionId: string = sessionPayload?.sessionId || (await getSessionId(instanceId)) || ''; + const signedIn: boolean = sessionPayload ? true : await isSignedIn(sessionId, instanceId); let user: User = {}; let userProfile: UserProfile = { @@ -138,20 +146,20 @@ const AsgardeoServerProvider: FC> data: {user: User | null}; error: string | null; success: boolean; - } = await getUserAction(sessionId); + } = await getUserAction(sessionId, instanceId); const userProfileResponse: { data: {userProfile: UserProfile}; error: string | null; success: boolean; - } = await getUserProfileAction(sessionId); + } = await getUserProfileAction(sessionId, instanceId); const currentOrganizationResponse: { data: {organization?: Organization; user?: Record}; error: string | null; success: boolean; - } = await getCurrentOrganizationAction(sessionId); + } = await getCurrentOrganizationAction(sessionId, instanceId); if (sessionId) { - myOrganizations = await getMyOrganizations({}, sessionId); + myOrganizations = await getMyOrganizations({}, sessionId, instanceId); } else { // eslint-disable-next-line no-console console.warn('[AsgardeoServerProvider] No session ID available, skipping organization fetch'); @@ -186,15 +194,26 @@ const AsgardeoServerProvider: FC> } } + // Create instance-bound server actions using .bind() so React can serialize them + // as proper server action references when passed to Client Components. + const boundSignIn = signInAction.bind(null, instanceId); + const boundSignOut = signOutAction.bind(null, instanceId); + const boundSignUp = signUpAction.bind(null, instanceId); + const boundHandleOAuthCallback = handleOAuthCallbackAction.bind(null, instanceId); + const boundSwitchOrganization = switchOrganization.bind(null, instanceId); + const boundGetAllOrganizations = getAllOrganizations.bind(null, instanceId); + const boundCreateOrganization = createOrganization.bind(null, instanceId); + const boundUpdateUserProfile = updateUserProfileAction.bind(null, instanceId); + return ( > user={user} currentOrganization={currentOrganization} userProfile={userProfile} - updateProfile={updateUserProfileAction} + updateProfile={boundUpdateUserProfile} isSignedIn={signedIn} myOrganizations={myOrganizations} - getAllOrganizations={getAllOrganizations} - switchOrganization={switchOrganization} + getAllOrganizations={boundGetAllOrganizations} + switchOrganization={boundSwitchOrganization} brandingPreference={brandingPreference} - createOrganization={createOrganization} + createOrganization={boundCreateOrganization} + instanceId={instanceId} > {children} diff --git a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts b/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts index 6367ce537..93e904e8a 100644 --- a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts @@ -77,7 +77,7 @@ describe('createOrganization (Next.js server action)', () => { it('should create an organization successfully when a sessionId is provided', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result: Organization = await createOrganization(basePayload, 'sess-123'); + const result: Organization = await createOrganization(0, basePayload, 'sess-123'); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(getSessionId).not.toHaveBeenCalled(); @@ -88,7 +88,7 @@ describe('createOrganization (Next.js server action)', () => { it('should fall back to getSessionId when sessionId is undefined', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result: Organization = await createOrganization(basePayload, undefined as unknown as string); + const result: Organization = await createOrganization(0, basePayload, undefined as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); @@ -98,7 +98,7 @@ describe('createOrganization (Next.js server action)', () => { it('should fall back to getSessionId when sessionId is null', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result: Organization = await createOrganization(basePayload, null as unknown as string); + const result: Organization = await createOrganization(0, basePayload, null as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); @@ -108,7 +108,7 @@ describe('createOrganization (Next.js server action)', () => { it('should not call getSessionId when an empty string is passed (empty string is not nullish)', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result: Organization = await createOrganization(basePayload, ''); + const result: Organization = await createOrganization(0, basePayload, ''); expect(getSessionId).not.toHaveBeenCalled(); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, ''); @@ -124,7 +124,7 @@ describe('createOrganization (Next.js server action)', () => { ); mockClient.createOrganization.mockRejectedValueOnce(original); - await expect(createOrganization(basePayload, 'sess-1')).rejects.toMatchObject({ + await expect(createOrganization(0, basePayload, 'sess-1')).rejects.toMatchObject({ constructor: AsgardeoAPIError, message: expect.stringContaining('Failed to create the organization: Upstream validation failed'), statusCode: 400, diff --git a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts b/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts index 196248c09..5f79c8bd5 100644 --- a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts @@ -71,7 +71,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('returns organizations when a sessionId is provided (no getSessionId fallback)', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, 'sess-123'); + const result: AllOrganizationsApiResponse = await getAllOrganizations(0, baseOptions, 'sess-123'); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(getSessionId).not.toHaveBeenCalled(); @@ -82,7 +82,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('falls back to getSessionId when sessionId is undefined', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, undefined); + const result: AllOrganizationsApiResponse = await getAllOrganizations(0, baseOptions, undefined); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); @@ -92,7 +92,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('falls back to getSessionId when sessionId is null', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, null as unknown as string); + const result: AllOrganizationsApiResponse = await getAllOrganizations(0, baseOptions, null as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); @@ -102,7 +102,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('does not call getSessionId for an empty string sessionId (empty string is not nullish)', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, ''); + const result: AllOrganizationsApiResponse = await getAllOrganizations(0, baseOptions, ''); expect(getSessionId).not.toHaveBeenCalled(); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, ''); @@ -113,7 +113,7 @@ describe('getAllOrganizations (Next.js server action)', () => { const upstream: AsgardeoAPIError = new AsgardeoAPIError('Upstream failed', 'ORG_LIST_500', 'server', 503); mockClient.getAllOrganizations.mockRejectedValueOnce(upstream); - await expect(getAllOrganizations(baseOptions, 'sess-x')).rejects.toMatchObject({ + await expect(getAllOrganizations(0, baseOptions, 'sess-x')).rejects.toMatchObject({ constructor: AsgardeoAPIError, message: expect.stringContaining('Failed to get all the organizations for the user: Upstream failed'), statusCode: 503, diff --git a/packages/nextjs/src/server/actions/createOrganization.ts b/packages/nextjs/src/server/actions/createOrganization.ts index 49dc852b4..c0f779dae 100644 --- a/packages/nextjs/src/server/actions/createOrganization.ts +++ b/packages/nextjs/src/server/actions/createOrganization.ts @@ -25,10 +25,10 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to create an organization. */ -const createOrganization = async (payload: CreateOrganizationPayload, sessionId: string): Promise => { +const createOrganization = async (instanceId: number = 0, payload: CreateOrganizationPayload, sessionId: string): Promise => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - return await client.createOrganization(payload, sessionId ?? ((await getSessionId()) as string)); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); + return await client.createOrganization(payload, sessionId ?? ((await getSessionId(instanceId)) as string)); } catch (error) { throw new AsgardeoAPIError( `Failed to create the organization: ${error instanceof Error ? error.message : String(error)}`, diff --git a/packages/nextjs/src/server/actions/getAccessToken.ts b/packages/nextjs/src/server/actions/getAccessToken.ts index 9d7a351fe..e4f009688 100644 --- a/packages/nextjs/src/server/actions/getAccessToken.ts +++ b/packages/nextjs/src/server/actions/getAccessToken.ts @@ -27,10 +27,10 @@ import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; * * @returns The access token if it exists, undefined otherwise */ -const getAccessToken = async (): Promise => { +const getAccessToken = async (instanceId: number = 0): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName(instanceId))?.value; if (sessionToken) { try { diff --git a/packages/nextjs/src/server/actions/getAllOrganizations.ts b/packages/nextjs/src/server/actions/getAllOrganizations.ts index 4fcbef8d3..751abceb8 100644 --- a/packages/nextjs/src/server/actions/getAllOrganizations.ts +++ b/packages/nextjs/src/server/actions/getAllOrganizations.ts @@ -26,12 +26,13 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; * Server action to get organizations. */ const getAllOrganizations = async ( + instanceId: number = 0, options?: any, sessionId?: string | undefined, ): Promise => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - return await client.getAllOrganizations(options, sessionId ?? ((await getSessionId()) as string)); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); + return await client.getAllOrganizations(options, sessionId ?? ((await getSessionId(instanceId)) as string)); } catch (error) { throw new AsgardeoAPIError( `Failed to get all the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, diff --git a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts index a355ab90c..172d382b6 100644 --- a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts +++ b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts @@ -26,13 +26,14 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; */ const getCurrentOrganizationAction = async ( sessionId: string, + instanceId: number = 0, ): Promise<{ data: {organization?: Organization; user?: Record}; error: string | null; success: boolean; }> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const organization: Organization = (await client.getCurrentOrganization(sessionId)) as Organization; return {data: {organization}, error: null, success: true}; } catch (error) { diff --git a/packages/nextjs/src/server/actions/getMyOrganizations.ts b/packages/nextjs/src/server/actions/getMyOrganizations.ts index eb9f62aee..ef74a704c 100644 --- a/packages/nextjs/src/server/actions/getMyOrganizations.ts +++ b/packages/nextjs/src/server/actions/getMyOrganizations.ts @@ -24,16 +24,16 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to get organizations. */ -const getMyOrganizations = async (options?: any, sessionId?: string | undefined): Promise => { +const getMyOrganizations = async (options?: any, sessionId?: string | undefined, instanceId: number = 0): Promise => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); // Get session ID if not provided let resolvedSessionId: string | undefined = sessionId; if (!resolvedSessionId) { // Import getSessionId locally to avoid circular dependencies const {default: getSessionId} = await import('./getSessionId'); - resolvedSessionId = await getSessionId(); + resolvedSessionId = await getSessionId(instanceId); } if (!resolvedSessionId) { diff --git a/packages/nextjs/src/server/actions/getOrganizationAction.ts b/packages/nextjs/src/server/actions/getOrganizationAction.ts index d83491e17..ebb955665 100644 --- a/packages/nextjs/src/server/actions/getOrganizationAction.ts +++ b/packages/nextjs/src/server/actions/getOrganizationAction.ts @@ -27,13 +27,14 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; const getOrganizationAction = async ( organizationId: string, sessionId: string, + instanceId: number = 0, ): Promise<{ data: {organization?: OrganizationDetails; user?: Record}; error: string | null; success: boolean; }> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const organization: OrganizationDetails = await client.getOrganization(organizationId, sessionId); return {data: {organization}, error: null, success: true}; } catch (error) { diff --git a/packages/nextjs/src/server/actions/getSessionId.ts b/packages/nextjs/src/server/actions/getSessionId.ts index e3b598c30..5bd55597f 100644 --- a/packages/nextjs/src/server/actions/getSessionId.ts +++ b/packages/nextjs/src/server/actions/getSessionId.ts @@ -28,10 +28,10 @@ import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; * * @returns The session ID if it exists, undefined otherwise */ -const getSessionId = async (): Promise => { +const getSessionId = async (instanceId: number = 0): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName(instanceId))?.value; if (sessionToken) { try { diff --git a/packages/nextjs/src/server/actions/getSessionPayload.ts b/packages/nextjs/src/server/actions/getSessionPayload.ts index 7cdea1d73..f8eec6221 100644 --- a/packages/nextjs/src/server/actions/getSessionPayload.ts +++ b/packages/nextjs/src/server/actions/getSessionPayload.ts @@ -28,10 +28,10 @@ import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; * * @returns The session payload if valid JWT session exists, undefined otherwise */ -const getSessionPayload = async (): Promise => { +const getSessionPayload = async (instanceId: number = 0): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName(instanceId))?.value; if (!sessionToken) { return undefined; } diff --git a/packages/nextjs/src/server/actions/getUserAction.ts b/packages/nextjs/src/server/actions/getUserAction.ts index a0ad5e4ba..97664cca4 100644 --- a/packages/nextjs/src/server/actions/getUserAction.ts +++ b/packages/nextjs/src/server/actions/getUserAction.ts @@ -27,9 +27,10 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; */ const getUserAction = async ( sessionId: string, + instanceId: number = 0, ): Promise<{data: {user: User | null}; error: string | null; success: boolean}> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const user: User = await client.getUser(sessionId); return {data: {user}, error: null, success: true}; } catch (error) { diff --git a/packages/nextjs/src/server/actions/getUserProfileAction.ts b/packages/nextjs/src/server/actions/getUserProfileAction.ts index 983bc0375..5d6180455 100644 --- a/packages/nextjs/src/server/actions/getUserProfileAction.ts +++ b/packages/nextjs/src/server/actions/getUserProfileAction.ts @@ -27,9 +27,10 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; */ const getUserProfileAction = async ( sessionId: string, + instanceId: number = 0, ): Promise<{data: {userProfile: UserProfile}; error: string | null; success: boolean}> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const updatedProfile: UserProfile = await client.getUserProfile(sessionId); return {data: {userProfile: updatedProfile}, error: null, success: true}; } catch (error) { diff --git a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts index 5acfdbbb1..23266172f 100644 --- a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts +++ b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts @@ -37,6 +37,7 @@ import SessionManager from '../../utils/SessionManager'; * @returns Promise that resolves with success status and optional error message */ const handleOAuthCallbackAction = async ( + instanceId: number = 0, code: string, state: string, sessionState?: string, @@ -53,7 +54,7 @@ const handleOAuthCallbackAction = async ( }; } - const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); if (!asgardeoClient.isInitialized) { return { @@ -65,7 +66,7 @@ const handleOAuthCallbackAction = async ( const cookieStore: ReadonlyRequestCookies = await cookies(); let sessionId: string | undefined; - const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName(instanceId))?.value; if (tempSessionToken) { try { @@ -119,9 +120,9 @@ const handleOAuthCallbackAction = async ( organizationId, ); - cookieStore.set(SessionManager.getSessionCookieName(), sessionToken, SessionManager.getSessionCookieOptions()); + cookieStore.set(SessionManager.getSessionCookieName(instanceId), sessionToken, SessionManager.getSessionCookieOptions()); - cookieStore.delete(SessionManager.getTempSessionCookieName()); + cookieStore.delete(SessionManager.getTempSessionCookieName(instanceId)); } catch (error) { logger.error( `[handleOAuthCallbackAction] Failed to create JWT session, continuing with legacy session: diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index 6c0d43780..9b2d9db3f 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -30,15 +30,15 @@ import {SessionTokenPayload} from '../../utils/SessionManager'; * @param sessionId - Optional session ID to check (if not provided, gets from cookies) * @returns True if user is signed in, false otherwise */ -const isSignedIn = async (sessionId?: string): Promise => { +const isSignedIn = async (sessionId?: string, instanceId: number = 0): Promise => { try { - const sessionPayload: SessionTokenPayload | undefined = await getSessionPayload(); + const sessionPayload: SessionTokenPayload | undefined = await getSessionPayload(instanceId); if (sessionPayload) { const resolvedSessionId: string = sessionPayload.sessionId; if (resolvedSessionId) { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); try { const accessToken: string = await client.getAccessToken(resolvedSessionId); return !!accessToken; @@ -48,13 +48,13 @@ const isSignedIn = async (sessionId?: string): Promise => { } } - const resolvedSessionId: string | undefined = sessionId || (await getSessionId()); + const resolvedSessionId: string | undefined = sessionId || (await getSessionId(instanceId)); if (!resolvedSessionId) { return false; } - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); try { const accessToken: string = await client.getAccessToken(resolvedSessionId); diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index 98ca3412e..b7d210f57 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -41,6 +41,7 @@ import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; * @returns Promise that resolves when sign-in is complete */ const signInAction = async ( + instanceId: number = 0, payload?: EmbeddedSignInFlowHandleRequestPayload, request?: EmbeddedFlowExecuteRequestConfig, ): Promise<{ @@ -54,12 +55,12 @@ const signInAction = async ( success: boolean; }> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const cookieStore: ReadonlyRequestCookies = await cookies(); let sessionId: string | undefined; - const existingSessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const existingSessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName(instanceId))?.value; if (existingSessionToken) { try { @@ -71,7 +72,7 @@ const signInAction = async ( } if (!sessionId) { - const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName(instanceId))?.value; if (tempSessionToken) { try { @@ -89,7 +90,7 @@ const signInAction = async ( const tempSessionToken: string = await SessionManager.createTempSession(sessionId); cookieStore.set( - SessionManager.getTempSessionCookieName(), + SessionManager.getTempSessionCookieName(instanceId), tempSessionToken, SessionManager.getTempSessionCookieOptions(), ); @@ -132,9 +133,9 @@ const signInAction = async ( organizationId, ); - cookieStore.set(SessionManager.getSessionCookieName(), sessionToken, SessionManager.getSessionCookieOptions()); + cookieStore.set(SessionManager.getSessionCookieName(instanceId), sessionToken, SessionManager.getSessionCookieOptions()); - cookieStore.delete(SessionManager.getTempSessionCookieName()); + cookieStore.delete(SessionManager.getTempSessionCookieName(instanceId)); } const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); diff --git a/packages/nextjs/src/server/actions/signOutAction.ts b/packages/nextjs/src/server/actions/signOutAction.ts index 6d76e13ea..e323d2537 100644 --- a/packages/nextjs/src/server/actions/signOutAction.ts +++ b/packages/nextjs/src/server/actions/signOutAction.ts @@ -31,19 +31,19 @@ import SessionManager from '../../utils/SessionManager'; * * @returns Promise that resolves with success status and optional after sign-out URL */ -const signOutAction = async (): Promise<{data?: {afterSignOutUrl?: string}; error?: unknown; success: boolean}> => { +const signOutAction = async (instanceId: number = 0): Promise<{data?: {afterSignOutUrl?: string}; error?: unknown; success: boolean}> => { logger.debug('[signOutAction] Initiating sign out process from the server action.'); const clearSessionCookies = async (): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - cookieStore.delete(SessionManager.getSessionCookieName()); - cookieStore.delete(SessionManager.getTempSessionCookieName()); + cookieStore.delete(SessionManager.getSessionCookieName(instanceId)); + cookieStore.delete(SessionManager.getTempSessionCookieName(instanceId)); }; try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - const sessionId: string | undefined = await getSessionId(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); + const sessionId: string | undefined = await getSessionId(instanceId); let afterSignOutUrl: string = '/'; diff --git a/packages/nextjs/src/server/actions/signUpAction.ts b/packages/nextjs/src/server/actions/signUpAction.ts index febd32c03..4e208a362 100644 --- a/packages/nextjs/src/server/actions/signUpAction.ts +++ b/packages/nextjs/src/server/actions/signUpAction.ts @@ -30,6 +30,7 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; * @returns Promise that resolves when sign-in is complete */ const signUpAction = async ( + instanceId: number = 0, payload?: EmbeddedFlowExecuteRequestPayload, ): Promise<{ data?: @@ -42,7 +43,7 @@ const signUpAction = async ( success: boolean; }> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); // If no payload provided, redirect to sign-in URL for redirect-based sign-in. // If there's a payload, handle the embedded sign-in flow. diff --git a/packages/nextjs/src/server/actions/switchOrganization.ts b/packages/nextjs/src/server/actions/switchOrganization.ts index 64d095051..fa2d0e700 100644 --- a/packages/nextjs/src/server/actions/switchOrganization.ts +++ b/packages/nextjs/src/server/actions/switchOrganization.ts @@ -30,13 +30,14 @@ import SessionManager from '../../utils/SessionManager'; * Server action to switch organization. */ const switchOrganization = async ( + instanceId: number = 0, organization: Organization, sessionId: string | undefined, ): Promise => { try { const cookieStore: ReadonlyRequestCookies = await cookies(); - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - const resolvedSessionId: string = sessionId ?? ((await getSessionId()) as string); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); + const resolvedSessionId: string = sessionId ?? ((await getSessionId(instanceId)) as string); const response: TokenResponse | Response = await client.switchOrganization(organization, resolvedSessionId); // After switching organization, we need to refresh the page to get updated session data @@ -65,7 +66,7 @@ const switchOrganization = async ( logger.debug('[switchOrganization] Session token created successfully.'); - cookieStore.set(SessionManager.getSessionCookieName(), sessionToken, SessionManager.getSessionCookieOptions()); + cookieStore.set(SessionManager.getSessionCookieName(instanceId), sessionToken, SessionManager.getSessionCookieOptions()); } return response; diff --git a/packages/nextjs/src/server/actions/updateUserProfileAction.ts b/packages/nextjs/src/server/actions/updateUserProfileAction.ts index 33937b376..bdde00135 100644 --- a/packages/nextjs/src/server/actions/updateUserProfileAction.ts +++ b/packages/nextjs/src/server/actions/updateUserProfileAction.ts @@ -26,11 +26,12 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; * Returns the user profile if signed in. */ const updateUserProfileAction = async ( + instanceId: number = 0, payload: UpdateMeProfileConfig, sessionId?: string, ): Promise<{data: {user: User}; error: string; success: boolean}> => { try { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); const user: User = await client.updateUserProfile(payload, sessionId); return {data: {user}, error: '', success: true}; } catch (error) { diff --git a/packages/nextjs/src/server/asgardeo.ts b/packages/nextjs/src/server/asgardeo.ts index 348dca150..ea35f5af4 100644 --- a/packages/nextjs/src/server/asgardeo.ts +++ b/packages/nextjs/src/server/asgardeo.ts @@ -21,29 +21,29 @@ import getSessionIdAction from './actions/getSessionId'; import AsgardeoNextClient from '../AsgardeoNextClient'; import {AsgardeoNextConfig} from '../models/config'; -const asgardeo = async (): Promise<{ +const asgardeo = async (instanceId: number = 0): Promise<{ exchangeToken: (config: TokenExchangeRequestConfig, sessionId: string) => Promise; getAccessToken: (sessionId: string) => Promise; getSessionId: () => Promise; reInitialize: (config: Partial) => Promise; }> => { const getAccessToken = async (sessionId: string): Promise => { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); return client.getAccessToken(sessionId); }; - const getSessionId = async (): Promise => getSessionIdAction(); + const getSessionId = async (): Promise => getSessionIdAction(instanceId); const exchangeToken = async ( config: TokenExchangeRequestConfig, sessionId: string, ): Promise => { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); return client.exchangeToken(config, sessionId); }; const reInitialize = async (config: Partial): Promise => { - const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(instanceId); return client.reInitialize(config); }; diff --git a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts index da9e33bcb..c540c2a41 100644 --- a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts +++ b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts @@ -25,13 +25,15 @@ import { getSessionIdFromRequest, } from '../../utils/sessionUtils'; -export type AsgardeoMiddlewareOptions = Partial; +export type AsgardeoMiddlewareOptions = Partial & {instanceId?: number}; export type AsgardeoMiddlewareContext = { /** Get the session payload from JWT session if available */ getSession: () => Promise; /** Get the session ID from the current request */ getSessionId: () => string | undefined; + /** The instance ID for this middleware context */ + instanceId: number; /** Check if the current request has a valid Asgardeo session */ isSignedIn: () => boolean; /** @@ -57,9 +59,9 @@ type AsgardeoMiddlewareHandler = ( * @param request - The Next.js request object * @returns True if a valid session exists, false otherwise */ -const hasValidSession = async (request: NextRequest): Promise => { +const hasValidSession = async (request: NextRequest, instanceId: number = 0): Promise => { try { - return await hasValidJWTSession(request); + return await hasValidJWTSession(request, instanceId); } catch { return Promise.resolve(false); } @@ -72,8 +74,8 @@ const hasValidSession = async (request: NextRequest): Promise => { * @param request - The Next.js request object * @returns The session ID if it exists, undefined otherwise */ -const getSessionIdFromRequestMiddleware = async (request: NextRequest): Promise => - getSessionIdFromRequest(request); +const getSessionIdFromRequestMiddleware = async (request: NextRequest, instanceId: number = 0): Promise => + getSessionIdFromRequest(request, instanceId); /** * Asgardeo middleware that integrates authentication into your Next.js application. @@ -138,6 +140,7 @@ const asgardeoMiddleware = ): ((request: NextRequest) => Promise) => async (request: NextRequest): Promise => { const resolvedOptions: AsgardeoMiddlewareOptions = typeof options === 'function' ? options(request) : options || {}; + const instanceId: number = resolvedOptions.instanceId ?? 0; const url: URL = new URL(request.url); const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state'); @@ -150,7 +153,7 @@ const asgardeoMiddleware = if (!hasError) { // Validate that there's a temporary session that initiated this OAuth flow const tempSessionToken: string | undefined = request.cookies.get( - SessionManager.getTempSessionCookieName(), + SessionManager.getTempSessionCookieName(instanceId), )?.value; if (tempSessionToken) { try { @@ -165,18 +168,19 @@ const asgardeoMiddleware = } } - const sessionId: string | undefined = await getSessionIdFromRequestMiddleware(request); - const isAuthenticated: boolean = await hasValidSession(request); + const sessionId: string | undefined = await getSessionIdFromRequestMiddleware(request, instanceId); + const isAuthenticated: boolean = await hasValidSession(request, instanceId); const asgardeo: AsgardeoMiddlewareContext = { getSession: async (): Promise => { try { - return await getSessionFromRequest(request); + return await getSessionFromRequest(request, instanceId); } catch { return undefined; } }, getSessionId: (): string | undefined => sessionId, + instanceId, isSignedIn: (): boolean => isAuthenticated, // eslint-disable-next-line @typescript-eslint/no-unused-vars protectRoute: async (routeOptions?: {redirect?: string}): Promise => { diff --git a/packages/nextjs/src/utils/SessionManager.ts b/packages/nextjs/src/utils/SessionManager.ts index a83cceb09..494428664 100644 --- a/packages/nextjs/src/utils/SessionManager.ts +++ b/packages/nextjs/src/utils/SessionManager.ts @@ -198,15 +198,21 @@ class SessionManager { /** * Get session cookie name */ - static getSessionCookieName(): string { - return CookieConfig.SESSION_COOKIE_NAME; + static getSessionCookieName(instanceId: number = 0): string { + if (instanceId === 0) { + return CookieConfig.SESSION_COOKIE_NAME; + } + return `${CookieConfig.SESSION_COOKIE_NAME}.${instanceId}`; } /** * Get temporary session cookie name */ - static getTempSessionCookieName(): string { - return CookieConfig.TEMP_SESSION_COOKIE_NAME; + static getTempSessionCookieName(instanceId: number = 0): string { + if (instanceId === 0) { + return CookieConfig.TEMP_SESSION_COOKIE_NAME; + } + return `${CookieConfig.TEMP_SESSION_COOKIE_NAME}.${instanceId}`; } } diff --git a/packages/nextjs/src/utils/sessionUtils.ts b/packages/nextjs/src/utils/sessionUtils.ts index 2aa572173..57eeb6356 100644 --- a/packages/nextjs/src/utils/sessionUtils.ts +++ b/packages/nextjs/src/utils/sessionUtils.ts @@ -26,9 +26,9 @@ import SessionManager, {SessionTokenPayload} from './SessionManager'; * @param request - The Next.js request object * @returns True if a valid session exists, false otherwise */ -export const hasValidSession = async (request: NextRequest): Promise => { +export const hasValidSession = async (request: NextRequest, instanceId: number = 0): Promise => { try { - const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName(instanceId))?.value; if (!sessionToken) { return false; } @@ -47,9 +47,9 @@ export const hasValidSession = async (request: NextRequest): Promise => * @param request - The Next.js request object * @returns The session payload if valid, undefined otherwise */ -export const getSessionFromRequest = async (request: NextRequest): Promise => { +export const getSessionFromRequest = async (request: NextRequest, instanceId: number = 0): Promise => { try { - const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName(instanceId))?.value; if (!sessionToken) { return undefined; } @@ -67,9 +67,9 @@ export const getSessionFromRequest = async (request: NextRequest): Promise => { +export const getSessionIdFromRequest = async (request: NextRequest, instanceId: number = 0): Promise => { try { - const sessionPayload: SessionTokenPayload | undefined = await getSessionFromRequest(request); + const sessionPayload: SessionTokenPayload | undefined = await getSessionFromRequest(request, instanceId); if (sessionPayload) { return sessionPayload.sessionId; @@ -87,9 +87,9 @@ export const getSessionIdFromRequest = async (request: NextRequest): Promise => { +export const getTempSessionFromRequest = async (request: NextRequest, instanceId: number = 0): Promise => { try { - const tempToken: string | undefined = request.cookies.get(SessionManager.getTempSessionCookieName())?.value; + const tempToken: string | undefined = request.cookies.get(SessionManager.getTempSessionCookieName(instanceId))?.value; if (!tempToken) { return undefined; } diff --git a/packages/node/src/__legacy__/client.ts b/packages/node/src/__legacy__/client.ts index 2fdfe7ad9..44bdef5b0 100644 --- a/packages/node/src/__legacy__/client.ts +++ b/packages/node/src/__legacy__/client.ts @@ -63,8 +63,8 @@ export class AsgardeoNodeClient { // eslint-disable-next-line @typescript-eslint/no-empty-function constructor() {} - public async initialize(config: AuthClientConfig, store?: Storage): Promise { - this.authCore = new AsgardeoNodeCore(config, store); + public async initialize(config: AuthClientConfig, store?: Storage, instanceId?: number): Promise { + this.authCore = new AsgardeoNodeCore(config, store, instanceId); return Promise.resolve(true); } diff --git a/packages/node/src/__legacy__/core/authentication.ts b/packages/node/src/__legacy__/core/authentication.ts index f5edfebab..0c0007b32 100644 --- a/packages/node/src/__legacy__/core/authentication.ts +++ b/packages/node/src/__legacy__/core/authentication.ts @@ -44,7 +44,7 @@ export class AsgardeoNodeCore { private storageManager: StorageManager; - constructor(config: AuthClientConfig, store?: Storage) { + constructor(config: AuthClientConfig, store?: Storage, instanceId?: number) { // Initialize the default memory cache store if an external store is not passed. if (!store) { this.store = new MemoryCacheStore(); @@ -53,7 +53,7 @@ export class AsgardeoNodeCore { } this.cryptoUtils = new NodeCryptoUtils(); this.auth = new AsgardeoAuthClient(); - this.auth.initialize(config, this.store, this.cryptoUtils); + this.auth.initialize(config, this.store, this.cryptoUtils, instanceId); this.storageManager = this.auth.getStorageManager(); Logger.debug('Initialized AsgardeoAuthClient successfully'); }