Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/loud-memes-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@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

---

Fix failure of calling authenticated APIs when using multiple AuthProvider instances
128 changes: 115 additions & 13 deletions packages/browser/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const DefaultConfig: Partial<AuthClientConfig<Config>> = {

/**
* This class provides the necessary methods to implement authentication in a Single Page Application.
* Implements a Multiton pattern to support multi-tenancy scenarios where multiple authentication
* contexts need to coexist in the same application.
*
* @export
* @class AsgardeoSPAClient
Expand Down Expand Up @@ -175,15 +177,22 @@ export class AsgardeoSPAClient {
}

/**
* This method returns the instance of the singleton class.
* This method returns the instance of the client for the specified ID.
* Implements a Multiton pattern to support multiple authentication contexts.
* If an ID is provided, it will return the instance with the given ID.
* If no ID is provided, it will return the default instance value 0.
* If no ID is provided, it will return the default instance (ID: 0).
*
* @return {AsgardeoSPAClient} - Returns the instance of the singleton class.
* @param {number} id - Optional unique identifier for the instance.
* @return {AsgardeoSPAClient} - Returns the instance associated with the ID.
*
* @example
* ```
* // Single tenant application (default instance)
* const auth = AsgardeoSPAClient.getInstance();
*
* // Multi-instance application
* const instance1 = AsgardeoSPAClient.getInstance(1);
* const instance2 = AsgardeoSPAClient.getInstance(2);
* ```
*
* @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getinstance
Expand All @@ -192,22 +201,115 @@ export class AsgardeoSPAClient {
*
* @preserve
*/
public static getInstance(id?: number): AsgardeoSPAClient | undefined {
if (id && this._instances?.get(id)) {
return this._instances.get(id);
} else if (!id && this._instances?.get(0)) {
return this._instances.get(0);
public static getInstance(id: number = 0): AsgardeoSPAClient {
if (!this._instances.has(id)) {
this._instances.set(id, new AsgardeoSPAClient(id));
}

if (id) {
this._instances.set(id, new AsgardeoSPAClient(id));
return this._instances.get(id)!;
}

/**
* This method checks if an instance exists for the given ID.
*
* @param {number} id - Optional unique identifier for the instance.
* @return {boolean} - Returns true if an instance exists for the ID.
*
* @example
* ```
* if (AsgardeoSPAClient.hasInstance(1)) {
* const auth = AsgardeoSPAClient.getInstance(1);
* }
* ```
*
* @memberof AsgardeoSPAClient
*/
public static hasInstance(id: number = 0): boolean {
return this._instances.has(id);
}

return this._instances.get(id);
/**
* This method removes and cleans up a specific instance.
* Useful when an instance is no longer needed.
*
* @param {number} id - Optional unique identifier for the instance to destroy.
* @return {boolean} - Returns true if the instance was found and removed.
*
* @example
* ```
* // Remove a specific instance
* AsgardeoSPAClient.destroyInstance(1);
*
* // Remove the default instance
* AsgardeoSPAClient.destroyInstance();
* ```
*
* @memberof AsgardeoSPAClient
*/
public static destroyInstance(id: number = 0): boolean {
const instance = this._instances.get(id);
if (instance) {
// Clean up the instance's session data before removing it
instance.clearSession();
return this._instances.delete(id);
}
return false;
}

this._instances.set(0, new AsgardeoSPAClient(0));
/**
* This method returns all active instance IDs.
* Useful for debugging or managing multiple instances.
*
* @return {number[]} - Returns an array of all active instance IDs.
*
* @example
* ```
* const activeInstances = AsgardeoSPAClient.getInstanceKeys();
* console.log('Active instances:', activeInstances);
* ```
*
* @memberof AsgardeoSPAClient
*/
public static getInstanceKeys(): number[] {
return Array.from(this._instances.keys());
}

return this._instances.get(0);
/**
* This method removes all instances.
* Useful for cleanup in testing scenarios or application teardown.
*
* @example
* ```
* AsgardeoSPAClient.destroyAllInstances();
* ```
*
* @memberof AsgardeoSPAClient
*/
public static destroyAllInstances(): void {
// Clean up each instance's session data before clearing
this._instances.forEach((instance) => {
instance.clearSession();
});
this._instances.clear();
}

/**
* This method returns the instance ID for this client instance.
*
* @return {number} - The instance ID.
*
* @example
* ```
* const auth = AsgardeoSPAClient.getInstance(1);
* console.log(auth.getInstanceId()); // 1
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public getInstanceId(): number {
return this._instanceID;
}

/**
Expand Down
61 changes: 42 additions & 19 deletions packages/browser/src/utils/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,53 @@
import {AsgardeoSPAClient} from '../__legacy__/client';

/**
* HTTP utility for making requests using the AsgardeoSPAClient instance.
* Creates an HTTP utility for making requests using a specific AsgardeoSPAClient instance.
*
* @param instanceId - Optional instance ID for multi-instance support. Defaults to 0.
* @returns An object with request and requestAll methods bound to the specified instance.
*
* @remarks
* This utility provides methods to make single or multiple HTTP requests.
* This utility provides methods to make single or multiple HTTP requests for a specific instance.
*
* @example
* ```typescript
* // Use default instance
* const httpClient = http();
*
* // Use specific instance
* const httpInstance1 = http(1);
* const httpInstance2 = http(2);
* ```
*/
const http: {
export const http = (
instanceId: number = 0,
): {
request: typeof AsgardeoSPAClient.prototype.httpRequest;
requestAll: typeof AsgardeoSPAClient.prototype.httpRequestAll;
} = {
/**
* Makes a single HTTP request using the AsgardeoSPAClient instance.
*
* @param config - The HTTP request configuration object.
* @returns A promise resolving to the HTTP response.
*/
request: AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()),
} => {
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);

/**
* Makes multiple HTTP requests in parallel using the AsgardeoSPAClient instance.
*
* @param configs - An array of HTTP request configuration objects.
* @returns A promise resolving to an array of HTTP responses.
*/
requestAll: AsgardeoSPAClient.getInstance().httpRequestAll.bind(AsgardeoSPAClient.getInstance()),
return {
/**
* Makes a single HTTP request using the AsgardeoSPAClient instance.
*
* @param config - The HTTP request configuration object.
* @returns A promise resolving to the HTTP response.
*/
request: client.httpRequest.bind(client),

/**
* Makes multiple HTTP requests in parallel using the AsgardeoSPAClient instance.
*
* @param configs - An array of HTTP request configuration objects.
* @returns A promise resolving to an array of HTTP responses.
*/
requestAll: client.httpRequestAll.bind(client),
};
};

export default http;
/**
* Default HTTP utility using instance 0.
* For multi-instance support, use http(instanceId) instead.
*/
export default http();
2 changes: 1 addition & 1 deletion packages/javascript/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class AsgardeoAuthClient<T> {
this.instanceIdValue += 1;
}

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;
}

this.instanceIdValue = instanceID;
}

Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/AsgardeoReactClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
baseUrl = configData?.baseUrl;
}

const profile: User = await getScim2Me({baseUrl});
const schemas: any = await getSchemas({baseUrl});
const profile: User = await getScim2Me({baseUrl, instanceId: this.getInstanceId()});
const schemas: any = await getSchemas({baseUrl, instanceId: this.getInstanceId()});

const processedSchemas: any = flattenUserSchema(schemas);

Expand Down Expand Up @@ -220,7 +220,7 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
baseUrl = configData?.baseUrl;
}

return await getMeOrganizations({baseUrl});
return await getMeOrganizations({baseUrl, instanceId: this.getInstanceId()});
} catch (error) {
throw new AsgardeoRuntimeError(
`Failed to fetch the user's associated organizations: ${
Expand All @@ -242,7 +242,7 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
baseUrl = configData?.baseUrl;
}

return await getAllOrganizations({baseUrl});
return await getAllOrganizations({baseUrl, instanceId: this.getInstanceId()});
} catch (error) {
throw new AsgardeoRuntimeError(
`Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`,
Expand Down
14 changes: 11 additions & 3 deletions packages/react/src/api/createOrganization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
CreateOrganizationConfig as BaseCreateOrganizationConfig,
} from '@asgardeo/browser';

const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());

/**
* Configuration for the createOrganization request (React-specific)
*/
Expand All @@ -37,6 +35,10 @@ export interface CreateOrganizationConfig extends Omit<BaseCreateOrganizationCon
* which is a wrapper around axios http.request
*/
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
/**
* Optional instance ID for multi-instance support. Defaults to 0.
*/
instanceId?: number;
}

/**
Expand Down Expand Up @@ -90,8 +92,14 @@ export interface CreateOrganizationConfig extends Omit<BaseCreateOrganizationCon
* }
* ```
*/
const createOrganization = async ({fetcher, ...requestConfig}: CreateOrganizationConfig): Promise<Organization> => {
const createOrganization = async ({
fetcher,
instanceId = 0,
...requestConfig
}: CreateOrganizationConfig): Promise<Organization> => {
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
const httpClient: HttpInstance = client.httpRequest.bind(client);
const response: HttpResponse<any> = await httpClient({
data: config.body ? JSON.parse(config.body as string) : undefined,
headers: config.headers as Record<string, string>,
Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/api/getAllOrganizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
AllOrganizationsApiResponse,
} from '@asgardeo/browser';

const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());

/**
* Configuration for the getAllOrganizations request (React-specific)
*/
Expand All @@ -37,6 +35,10 @@ export interface GetAllOrganizationsConfig extends Omit<BaseGetAllOrganizationsC
* which is a wrapper around axios http.request
*/
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
/**
* Optional instance ID for multi-instance support. Defaults to 0.
*/
instanceId?: number;
}

/**
Expand Down Expand Up @@ -84,9 +86,12 @@ export interface GetAllOrganizationsConfig extends Omit<BaseGetAllOrganizationsC
*/
const getAllOrganizations = async ({
fetcher,
instanceId = 0,
...requestConfig
}: GetAllOrganizationsConfig): Promise<AllOrganizationsApiResponse> => {
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
const httpClient: HttpInstance = client.httpRequest.bind(client);
const response: HttpResponse<any> = await httpClient({
headers: config.headers as Record<string, string>,
method: config.method || 'GET',
Expand Down
14 changes: 11 additions & 3 deletions packages/react/src/api/getMeOrganizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig,
} from '@asgardeo/browser';

const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());

/**
* Configuration for the getMeOrganizations request (React-specific)
*/
Expand All @@ -37,6 +35,10 @@ export interface GetMeOrganizationsConfig extends Omit<BaseGetMeOrganizationsCon
* which is a wrapper around axios http.request
*/
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
/**
* Optional instance ID for multi-instance support. Defaults to 0.
*/
instanceId?: number;
}

/**
Expand Down Expand Up @@ -86,8 +88,14 @@ export interface GetMeOrganizationsConfig extends Omit<BaseGetMeOrganizationsCon
* }
* ```
*/
const getMeOrganizations = async ({fetcher, ...requestConfig}: GetMeOrganizationsConfig): Promise<Organization[]> => {
const getMeOrganizations = async ({
fetcher,
instanceId = 0,
...requestConfig
}: GetMeOrganizationsConfig): Promise<Organization[]> => {
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
const httpClient: HttpInstance = client.httpRequest.bind(client);
const response: HttpResponse<any> = await httpClient({
headers: config.headers as Record<string, string>,
method: config.method || 'GET',
Expand Down
Loading
Loading