Skip to content

Fix failure of calling authenticated APIs when multiple AsgardeoProvider instances are used#334

Open
kavindadimuthu wants to merge 7 commits intoasgardeo:mainfrom
kavindadimuthu:refactor/singleton-to-multiton
Open

Fix failure of calling authenticated APIs when multiple AsgardeoProvider instances are used#334
kavindadimuthu wants to merge 7 commits intoasgardeo:mainfrom
kavindadimuthu:refactor/singleton-to-multiton

Conversation

@kavindadimuthu
Copy link
Contributor

@kavindadimuthu kavindadimuthu commented Jan 23, 2026

This pull request introduces adoption of a Multiton pattern in AsgardeoSPAClient, allowing multiple authentication contexts to coexist within the same application by managing client instances via unique IDs. Several React level APIs have been updated to accept and propagate these instance IDs, ensuring correct isolation and management of authentication state per instance.

Multiton pattern implementation and API updates for instance-awareness::

  • Refactored AsgardeoSPAClient in client.ts to implement a Multiton pattern, including new static methods for managing instances (getInstance, hasInstance, destroyInstance, getInstanceKeys, destroyAllInstances), and added an instance ID accessor (getInstanceId). [1] [2] [3]
  • Updated AsgardeoReactClient and all organization-related API functions (createOrganization, getAllOrganizations, getMeOrganizations, getOrganization) in React to accept an optional instanceId parameter and use the correct client instance for requests. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]

Related Issues

Related PRs

Checklist

  • Followed the CONTRIBUTING guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Unit tests provided. (Add links if there are any)

Security checks

@kavindadimuthu kavindadimuthu force-pushed the refactor/singleton-to-multiton branch from db06041 to 2af8495 Compare February 16, 2026 12:54
@asgardeo-github-bot
Copy link

🦋 Changeset detected

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Copy link
Contributor

@DonOmalVindula DonOmalVindula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes. Left some comments. Please address them before merging.

@DonOmalVindula
Copy link
Contributor

DonOmalVindula commented Feb 17, 2026

Issue: packages/browser/src/utils/http.ts was not updated

This file still has module-level getInstance() calls without an instanceId:

request: AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()),
requestAll: AsgardeoSPAClient.getInstance().httpRequestAll.bind(AsgardeoSPAClient.getInstance()),

This is the same pattern you fixed in all the packages/react/src/api/*.ts files. If anything uses this http util, it will always route to instance 0. Suggested fix — convert to a factory function that accepts instanceId:

const createHttp = (instanceId: number = 0) => {
  const client = AsgardeoSPAClient.getInstance(instanceId);
  return {
    request: client.httpRequest.bind(client),
    requestAll: client.httpRequestAll.bind(client),
  };
};

export default createHttp;

Or alternatively, if this is only used internally, pass the instanceId through from the caller.

@@ -250,6 +250,8 @@ export class AsgardeoAuthClient<T> {
authRequestConfig['client_secret'] = configData.clientSecret;
}

Copy link
Contributor

@DonOmalVindula DonOmalVindula Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is dead code. getAuthorizeRequestUrlParams already receives instanceId: this.getInstanceId().toString() below and handles the state parameter internally. Also, getAuthorizeRequestUrlParams explicitly filters out state from custom params:

if (key !== '' && value !== '' && key !== OIDCRequestConstants.Params.STATE) {

So this value is never used. Remove this line:

Suggested change

}

if (instanceID) {
if (instanceID !== undefined) {
Copy link
Contributor

@DonOmalVindula DonOmalVindula Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch fixing instanceID !== undefined here. But the block above (lines 124-128) still has the same falsy-zero problem:

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

If this.instanceIdValue is already 0, !this.instanceIdValue is true, so it resets to 0 instead of incrementing. Suggested fix — align with the cleaner logic in the browser package:

if (this.instanceIdValue === undefined || this.instanceIdValue === null) {
  this.instanceIdValue = 0;
} else {
  this.instanceIdValue += 1;
}

if (instanceID !== undefined) {
  this.instanceIdValue = instanceID;
}

Comment on lines +81 to +83
const getScim2Me = async ({fetcher, instanceId = 0, ...requestConfig}: GetScim2MeConfig): Promise<User> => {
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInstance is called twice. Store it once:

Suggested change
const getScim2Me = async ({fetcher, instanceId = 0, ...requestConfig}: GetScim2MeConfig): Promise<User> => {
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
const httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind(
const client = AsgardeoSPAClient.getInstance(instanceId);
const httpClient: HttpInstance = client.httpRequest.bind(client);

Check in other API files as well.

---
'@asgardeo/javascript': patch
'@asgardeo/browser': patch
'@asgardeo/react': patch
Copy link
Contributor

@DonOmalVindula DonOmalVindula Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of getInstance() changed from AsgardeoSPAClient | undefined to AsgardeoSPAClient, and new public methods were added (hasInstance, destroyInstance, getInstanceKeys, destroyAllInstances, getInstanceId). This is a public API surface change — should be a minor bump instead of patch:

Suggested change
'@asgardeo/react': patch
'@asgardeo/javascript': minor
'@asgardeo/browser': minor
'@asgardeo/react': patch

/**
* This method returns all active instance IDs.
* Useful for debugging or managing multiple instances.
*
Copy link
Contributor

@DonOmalVindula DonOmalVindula Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@preserve is only needed on methods that are part of the public API and should survive minification. Debugging/testing utilities like getInstanceKeys() and destroyAllInstances() don't need it. Remove @preserve from these utility methods. For example:

  /**
   * This method returns all active instance IDs.
   *
   * @return {number[]} - Returns an array of all active instance IDs.
   *
   * @memberof AsgardeoSPAClient
   */
  public static getInstanceKeys(): number[] {
    return Array.from(this._instances.keys());
  }

Apply the same for destroyAllInstances and hasInstance.

*
* // Remove the default instance
* AsgardeoSPAClient.destroyInstance();
* ```
Copy link
Contributor

@DonOmalVindula DonOmalVindula Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroyInstance only removes the instance from the map. It doesn't clean up the underlying auth state (storage, tokens, session data, etc.) that the instance manages. If someone calls destroyInstance(1) and then getInstance(1), they'll get a fresh instance but the old storage data for instance_1 is still sitting in session/local storage. Suggested fix:

  public static destroyInstance(id: number = 0): boolean {
    const instance = this._instances.get(id);
    if (instance) {
      // Clear session/storage data for this instance before removing
      instance.clearSession?.();
    }
    return this._instances.delete(id);
  }

Same applies for destroyAllInstances — iterate and clean up each instance before clearing the map.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Fails Authenticated API Calls When Multiple AsgardeoProvider Instances Are Used

3 participants