Skip to content
7 changes: 7 additions & 0 deletions .changeset/brown-paths-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@asgardeo/javascript': patch
'@asgardeo/browser': patch
'@asgardeo/react': patch
---

Fix login flow isolation when using multiple AuthProvider instances
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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';
Expand Down
32 changes: 32 additions & 0 deletions packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 9 additions & 8 deletions packages/javascript/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class AsgardeoAuthClient<T> {

private cryptoHelper: IsomorphicCrypto;

private static instanceIdValue: number;
private instanceIdValue: number;

// FIXME: Validate this.
// Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205
Expand Down Expand Up @@ -121,20 +121,20 @@ export class AsgardeoAuthClient<T> {
): Promise<void> {
const {clientId} = config;

if (!AsgardeoAuthClient.instanceIdValue) {
AsgardeoAuthClient.instanceIdValue = 0;
if (!this.instanceIdValue) {
this.instanceIdValue = 0;
} else {
AsgardeoAuthClient.instanceIdValue += 1;
this.instanceIdValue += 1;
}

if (instanceID) {
AsgardeoAuthClient.instanceIdValue = instanceID;
this.instanceIdValue = instanceID;
}

if (!clientId) {
this.storageManager = new StorageManager<T>(`instance_${AsgardeoAuthClient.instanceIdValue}`, store);
this.storageManager = new StorageManager<T>(`instance_${this.instanceIdValue}`, store);
} else {
this.storageManager = new StorageManager<T>(`instance_${AsgardeoAuthClient.instanceIdValue}-${clientId}`, store);
this.storageManager = new StorageManager<T>(`instance_${this.instanceIdValue}-${clientId}`, store);
}

this.cryptoUtils = inputCryptoUtils;
Expand Down Expand Up @@ -187,7 +187,7 @@ export class AsgardeoAuthClient<T> {
*/
// eslint-disable-next-line class-methods-use-this
public getInstanceId(): number {
return AsgardeoAuthClient.instanceIdValue;
return this.instanceIdValue;
}

/**
Expand Down Expand Up @@ -255,6 +255,7 @@ export class AsgardeoAuthClient<T> {
clientId: configData.clientId,
codeChallenge,
codeChallengeMethod: PKCEConstants.DEFAULT_CODE_CHALLENGE_METHOD,
instanceId: this.getInstanceId().toString(),
prompt: configData.prompt,
redirectUri: configData.afterSignInUrl,
responseMode: configData.responseMode,
Expand Down
15 changes: 11 additions & 4 deletions packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const getAuthorizeRequestUrlParams = (
clientId: string;
codeChallenge?: string;
codeChallengeMethod?: string;
instanceId?: string;
prompt?: string;
redirectUri: string;
responseMode?: string;
Expand Down Expand Up @@ -105,12 +106,18 @@ const getAuthorizeRequestUrlParams = (
});
}

const AUTH_INSTANCE_PREFIX: string = 'instance_';
let customStateValue: string = '';

if (options.instanceId) {
customStateValue = AUTH_INSTANCE_PREFIX + options.instanceId;
} else if (customParams) {
customStateValue = customParams[OIDCRequestConstants.Params.STATE]?.toString() ?? '';
}

authorizeRequestParams.set(
OIDCRequestConstants.Params.STATE,
generateStateParamForRequestCorrelation(
pkceKey,
customParams ? customParams[OIDCRequestConstants.Params.STATE]?.toString() : '',
),
generateStateParamForRequestCorrelation(pkceKey, customStateValue),
);

return authorizeRequestParams;
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
}: PropsWithChildren<AsgardeoProviderProps>): ReactElement => {
const reRenderCheckRef: RefObject<boolean> = useRef(false);
const asgardeo: AsgardeoReactClient = useMemo(() => new AsgardeoReactClient(instanceId), [instanceId]);
const {hasAuthParams} = useBrowserUrl();
const {hasAuthParams, hasCalledForThisInstance} = useBrowserUrl();
const [user, setUser] = useState<any | null>(null);
const [currentOrganization, setCurrentOrganization] = useState<Organization | null>(null);

Expand Down Expand Up @@ -266,7 +266,8 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
}

const currentUrl: URL = new URL(window.location.href);
const hasAuthParamsResult: boolean = hasAuthParams(currentUrl, afterSignInUrl);
const hasAuthParamsResult: boolean =
hasAuthParams(currentUrl, afterSignInUrl) && hasCalledForThisInstance(currentUrl, instanceId ?? 0);

const isV2Platform: boolean = config.platform === Platform.AsgardeoV2;

Expand Down
16 changes: 14 additions & 2 deletions packages/react/src/hooks/useBrowserUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand All @@ -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;
Loading