From e68a2eff78650288ba9e966566e9d0778010c95e Mon Sep 17 00:00:00 2001 From: kavindadewmith Date: Thu, 22 Jan 2026 14:17:26 +0530 Subject: [PATCH 1/2] Add support for multi-auth context with instance ID in AsgardeoReactClient and AuthAPI --- packages/react/src/AsgardeoReactClient.ts | 18 ++++++++++++++++-- packages/react/src/__temp__/api.ts | 14 ++++++++++++-- .../src/contexts/Asgardeo/AsgardeoProvider.tsx | 3 ++- packages/react/src/models/config.ts | 9 ++++++++- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 85d494f4..e581602d 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -68,12 +68,26 @@ import getAllOrganizations from './api/getAllOrganizations'; class AsgardeoReactClient extends AsgardeoBrowserClient { private asgardeo: AuthAPI; private _isLoading: boolean = false; + private _instanceId: number; - constructor() { + /** + * Creates a new AsgardeoReactClient instance. + * @param instanceId - Optional instance ID for multi-auth context support. Defaults to 0 for backward compatibility. + */ + constructor(instanceId: number = 0) { super(); + this._instanceId = instanceId; // FIXME: This has to be the browser client from `@asgardeo/browser` package. - this.asgardeo = new AuthAPI(); + this.asgardeo = new AuthAPI(undefined, instanceId); + } + + /** + * Get the instance ID for this client. + * @returns The instance ID used for multi-auth context support. + */ + public getInstanceId(): number { + return this._instanceId; } /** diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts index 35a21908..ca6064ee 100644 --- a/packages/react/src/__temp__/api.ts +++ b/packages/react/src/__temp__/api.ts @@ -37,11 +37,13 @@ class AuthAPI { private _authState = AuthAPI.DEFAULT_STATE; private _client: AsgardeoSPAClient; + private _instanceId: number; private _isLoading: boolean; - constructor(spaClient?: AsgardeoSPAClient) { - this._client = spaClient ?? AsgardeoSPAClient.getInstance(); + constructor(spaClient?: AsgardeoSPAClient, instanceId: number = 0) { + this._instanceId = instanceId; + this._client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId); this.getState = this.getState.bind(this); this.init = this.init.bind(this); @@ -50,6 +52,14 @@ class AuthAPI { this.updateState = this.updateState.bind(this); } + /** + * Get the instance ID for this AuthAPI instance. + * @returns The instance ID used for multi-auth context support. + */ + public getInstanceId(): number { + return this._instanceId; + } + public _setIsLoading(isLoading: boolean): void { this._isLoading = isLoading; } diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 797ea191..10f683ef 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -64,10 +64,11 @@ const AsgardeoProvider: FC> = ({ applicationId, signInOptions, syncSession, + instanceId = 0, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); - const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(), []); + const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(instanceId), [instanceId]); const {hasAuthParams} = useBrowserUrl(); const [user, setUser] = useState(null); const [currentOrganization, setCurrentOrganization] = useState(null); diff --git a/packages/react/src/models/config.ts b/packages/react/src/models/config.ts index f53fdd48..af4a37de 100644 --- a/packages/react/src/models/config.ts +++ b/packages/react/src/models/config.ts @@ -18,4 +18,11 @@ import {AsgardeoBrowserConfig} from '@asgardeo/browser'; -export type AsgardeoReactConfig = AsgardeoBrowserConfig; +export interface AsgardeoReactConfig extends AsgardeoBrowserConfig { + /** + * Optional instance ID for multi-auth context support. + * Use this when you need multiple authentication contexts in the same application. + * Defaults to 0 for backward compatibility. + */ + instanceId?: number; +} \ No newline at end of file From f3b72338722a393313f2d29aa724d9594dbe2264 Mon Sep 17 00:00:00 2001 From: kavindadewmith Date: Thu, 22 Jan 2026 14:45:58 +0530 Subject: [PATCH 2/2] Add utility to check instance-specific state in URL and refactor instance ID handling --- packages/browser/src/index.ts | 1 + .../utils/hasCalledForThisInstanceInUrl.ts | 32 +++++++++++++++++++ packages/javascript/src/__legacy__/client.ts | 18 ++++++----- .../contexts/Asgardeo/AsgardeoProvider.tsx | 4 +-- packages/react/src/hooks/useBrowserUrl.ts | 16 ++++++++-- 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 5b687a17..f1634fd8 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -46,6 +46,7 @@ export * from './__legacy__/worker/worker-receiver'; export {AsgardeoBrowserConfig} from './models/config'; export {default as hasAuthParamsInUrl} from './utils/hasAuthParamsInUrl'; +export {default as hasCalledForThisInstanceInUrl} from './utils/hasCalledForThisInstanceInUrl'; export {default as navigate} from './utils/navigate'; export {default as AsgardeoBrowserClient} from './AsgardeoBrowserClient'; diff --git a/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts b/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts new file mode 100644 index 00000000..3960e328 --- /dev/null +++ b/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts @@ -0,0 +1,32 @@ +/** + * 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. + */ + +/** + * Utility to check if `state` is available in the URL as a search param and matches the provided instance. + * + * @param params - The URL search params to check. Defaults to `window.location.search`. + * @param instanceId - The instance ID to match against the `state` param. + * @return `true` if the URL contains a matching `state` search param, otherwise `false`. + */ +const hasCalledForThisInstanceInUrl = (instanceId: number, params: string = window.location.search): boolean => { + const MATCHER: RegExp = new RegExp(`[?&]state=instance_${instanceId}-[^&]+`); + + return MATCHER.test(params); +}; + +export default hasCalledForThisInstanceInUrl; \ No newline at end of file diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index e54ddaec..2121655a 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -68,7 +68,7 @@ export class AsgardeoAuthClient { private _cryptoUtils: Crypto; private _cryptoHelper: IsomorphicCrypto; - private static _instanceID: number; + private _instanceID: number; // FIXME: Validate this. // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 @@ -118,20 +118,20 @@ export class AsgardeoAuthClient { ): Promise { const clientId: string = config.clientId; - if (!AsgardeoAuthClient._instanceID) { - AsgardeoAuthClient._instanceID = 0; + if (!this._instanceID) { + this._instanceID = 0; } else { - AsgardeoAuthClient._instanceID += 1; + this._instanceID += 1; } if (instanceID) { - AsgardeoAuthClient._instanceID = instanceID; + this._instanceID = instanceID; } if (!clientId) { - this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}`, store); + this._storageManager = new StorageManager(`instance_${this._instanceID}`, store); } else { - this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}-${clientId}`, store); + this._storageManager = new StorageManager(`instance_${this._instanceID}-${clientId}`, store); } this._cryptoUtils = cryptoUtils; @@ -182,7 +182,7 @@ export class AsgardeoAuthClient { * @preserve */ public getInstanceId(): number { - return AsgardeoAuthClient._instanceID; + return this._instanceID; } /** @@ -245,6 +245,8 @@ export class AsgardeoAuthClient { authRequestConfig['client_secret'] = configData.clientSecret; } + authRequestConfig['state'] = 'instance_' + this.getInstanceId() + '-' + configData.clientId; + const authorizeRequestParams: Map = getAuthorizeRequestUrlParams( { redirectUri: configData.afterSignInUrl, diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 10f683ef..e07380ff 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -69,7 +69,7 @@ const AsgardeoProvider: FC> = ({ }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(instanceId), [instanceId]); - const {hasAuthParams} = useBrowserUrl(); + const {hasAuthParams, hasCalledForThisInstance} = useBrowserUrl(); const [user, setUser] = useState(null); const [currentOrganization, setCurrentOrganization] = useState(null); @@ -154,7 +154,7 @@ const AsgardeoProvider: FC> = ({ } const currentUrl: URL = new URL(window.location.href); - const hasAuthParamsResult: boolean = hasAuthParams(currentUrl, afterSignInUrl); + const hasAuthParamsResult: boolean = hasAuthParams(currentUrl, afterSignInUrl) && hasCalledForThisInstance(currentUrl, instanceId ?? 1); const isV2Platform = config.platform === Platform.AsgardeoV2; diff --git a/packages/react/src/hooks/useBrowserUrl.ts b/packages/react/src/hooks/useBrowserUrl.ts index fd4bbd30..8a3a8a8c 100644 --- a/packages/react/src/hooks/useBrowserUrl.ts +++ b/packages/react/src/hooks/useBrowserUrl.ts @@ -16,7 +16,7 @@ * under the License. */ -import {hasAuthParamsInUrl} from '@asgardeo/browser'; +import {hasAuthParamsInUrl, hasCalledForThisInstanceInUrl} from '@asgardeo/browser'; /** * Interface for the useBrowserUrl hook return value. @@ -30,6 +30,15 @@ export interface UseBrowserUrl { * @returns True if the URL contains authentication parameters and matches the afterSignInUrl, or if it contains an error parameter */ hasAuthParams: (url: URL, afterSignInUrl: string) => boolean; + + /** + * Checks if the URL indicates that the authentication flow has been called for this instance. + * + * @param url - The URL object to check + * @param instanceId - The instance ID to check against + * @returns True if the URL indicates the flow has been called for this instance + */ + hasCalledForThisInstance: (url: URL, instanceId: number) => boolean; } /** @@ -53,7 +62,10 @@ const useBrowserUrl = (): UseBrowserUrl => { // authParams?.authorizationCode || // FIXME: These are sent externally. Need to see what we can do about this. url.searchParams.get('error') !== null; - return {hasAuthParams}; + const hasCalledForThisInstance = (url: URL, instanceId: number): boolean => + (hasCalledForThisInstanceInUrl(instanceId, url.search)); + + return {hasAuthParams, hasCalledForThisInstance}; }; export default useBrowserUrl;