diff --git a/.changeset/brown-paths-report.md b/.changeset/brown-paths-report.md new file mode 100644 index 00000000..f178f680 --- /dev/null +++ b/.changeset/brown-paths-report.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/browser': patch +'@asgardeo/react': patch +--- + +Fix login flow isolation when using multiple AuthProvider instances diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4f3b8e45..efbcdedf 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -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'; diff --git a/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts b/packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts new file mode 100644 index 00000000..3897221a --- /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; diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index d18daf98..ee254db8 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -69,7 +69,7 @@ export class AsgardeoAuthClient { private cryptoHelper: IsomorphicCrypto; - private static instanceIdValue: number; + private instanceIdValue: number; // FIXME: Validate this. // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 @@ -121,20 +121,20 @@ export class AsgardeoAuthClient { ): Promise { 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(`instance_${AsgardeoAuthClient.instanceIdValue}`, store); + this.storageManager = new StorageManager(`instance_${this.instanceIdValue}`, store); } else { - this.storageManager = new StorageManager(`instance_${AsgardeoAuthClient.instanceIdValue}-${clientId}`, store); + this.storageManager = new StorageManager(`instance_${this.instanceIdValue}-${clientId}`, store); } this.cryptoUtils = inputCryptoUtils; @@ -187,7 +187,7 @@ export class AsgardeoAuthClient { */ // eslint-disable-next-line class-methods-use-this public getInstanceId(): number { - return AsgardeoAuthClient.instanceIdValue; + return this.instanceIdValue; } /** @@ -255,6 +255,7 @@ export class AsgardeoAuthClient { clientId: configData.clientId, codeChallenge, codeChallengeMethod: PKCEConstants.DEFAULT_CODE_CHALLENGE_METHOD, + instanceId: this.getInstanceId().toString(), prompt: configData.prompt, redirectUri: configData.afterSignInUrl, responseMode: configData.responseMode, diff --git a/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts index daae2490..cf846655 100644 --- a/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts +++ b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts @@ -55,6 +55,7 @@ const getAuthorizeRequestUrlParams = ( clientId: string; codeChallenge?: string; codeChallengeMethod?: string; + instanceId?: string; prompt?: string; redirectUri: string; responseMode?: string; @@ -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; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 8fda0b4a..4131ea64 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -70,7 +70,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); @@ -266,7 +266,8 @@ 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 ?? 0); const isV2Platform: boolean = config.platform === Platform.AsgardeoV2; diff --git a/packages/react/src/hooks/useBrowserUrl.ts b/packages/react/src/hooks/useBrowserUrl.ts index fd4bbd30..9b0708a0 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;