Skip to content

Commit 2d6ad37

Browse files
Merge pull request asgardeo#334 from kavindadimuthu/refactor/singleton-to-multiton
Fix failure of calling authenticated APIs when multiple AsgardeoProvider instances are used
2 parents 3a811ac + 47cf07f commit 2d6ad37

18 files changed

Lines changed: 251 additions & 63 deletions

File tree

.changeset/fancy-humans-thank.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@asgardeo/javascript': minor
3+
'@asgardeo/browser': minor
4+
'@asgardeo/react': patch
5+
---
6+
7+
Fix failure of calling authenticated APIs when using multiple AuthProvider instances

packages/browser/src/__legacy__/client.ts

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const DefaultConfig: Partial<AuthClientConfig<Config>> = {
6767

6868
/**
6969
* This class provides the necessary methods to implement authentication in a Single Page Application.
70+
* Implements a Multiton pattern to support multi-tenancy scenarios where multiple authentication
71+
* contexts need to coexist in the same application.
7072
*
7173
* @export
7274
* @class AsgardeoSPAClient
@@ -175,15 +177,22 @@ export class AsgardeoSPAClient {
175177
}
176178

177179
/**
178-
* This method returns the instance of the singleton class.
180+
* This method returns the instance of the client for the specified ID.
181+
* Implements a Multiton pattern to support multiple authentication contexts.
179182
* If an ID is provided, it will return the instance with the given ID.
180-
* If no ID is provided, it will return the default instance value 0.
183+
* If no ID is provided, it will return the default instance (ID: 0).
181184
*
182-
* @return {AsgardeoSPAClient} - Returns the instance of the singleton class.
185+
* @param {number} id - Optional unique identifier for the instance.
186+
* @return {AsgardeoSPAClient} - Returns the instance associated with the ID.
183187
*
184188
* @example
185189
* ```
190+
* // Single tenant application (default instance)
186191
* const auth = AsgardeoSPAClient.getInstance();
192+
*
193+
* // Multi-instance application
194+
* const instance1 = AsgardeoSPAClient.getInstance(1);
195+
* const instance2 = AsgardeoSPAClient.getInstance(2);
187196
* ```
188197
*
189198
* @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getinstance
@@ -192,22 +201,115 @@ export class AsgardeoSPAClient {
192201
*
193202
* @preserve
194203
*/
195-
public static getInstance(id?: number): AsgardeoSPAClient | undefined {
196-
if (id && this._instances?.get(id)) {
197-
return this._instances.get(id);
198-
} else if (!id && this._instances?.get(0)) {
199-
return this._instances.get(0);
204+
public static getInstance(id: number = 0): AsgardeoSPAClient {
205+
if (!this._instances.has(id)) {
206+
this._instances.set(id, new AsgardeoSPAClient(id));
200207
}
201208

202-
if (id) {
203-
this._instances.set(id, new AsgardeoSPAClient(id));
209+
return this._instances.get(id)!;
210+
}
211+
212+
/**
213+
* This method checks if an instance exists for the given ID.
214+
*
215+
* @param {number} id - Optional unique identifier for the instance.
216+
* @return {boolean} - Returns true if an instance exists for the ID.
217+
*
218+
* @example
219+
* ```
220+
* if (AsgardeoSPAClient.hasInstance(1)) {
221+
* const auth = AsgardeoSPAClient.getInstance(1);
222+
* }
223+
* ```
224+
*
225+
* @memberof AsgardeoSPAClient
226+
*/
227+
public static hasInstance(id: number = 0): boolean {
228+
return this._instances.has(id);
229+
}
204230

205-
return this._instances.get(id);
231+
/**
232+
* This method removes and cleans up a specific instance.
233+
* Useful when an instance is no longer needed.
234+
*
235+
* @param {number} id - Optional unique identifier for the instance to destroy.
236+
* @return {boolean} - Returns true if the instance was found and removed.
237+
*
238+
* @example
239+
* ```
240+
* // Remove a specific instance
241+
* AsgardeoSPAClient.destroyInstance(1);
242+
*
243+
* // Remove the default instance
244+
* AsgardeoSPAClient.destroyInstance();
245+
* ```
246+
*
247+
* @memberof AsgardeoSPAClient
248+
*/
249+
public static destroyInstance(id: number = 0): boolean {
250+
const instance = this._instances.get(id);
251+
if (instance) {
252+
// Clean up the instance's session data before removing it
253+
instance.clearSession();
254+
return this._instances.delete(id);
206255
}
256+
return false;
257+
}
207258

208-
this._instances.set(0, new AsgardeoSPAClient(0));
259+
/**
260+
* This method returns all active instance IDs.
261+
* Useful for debugging or managing multiple instances.
262+
*
263+
* @return {number[]} - Returns an array of all active instance IDs.
264+
*
265+
* @example
266+
* ```
267+
* const activeInstances = AsgardeoSPAClient.getInstanceKeys();
268+
* console.log('Active instances:', activeInstances);
269+
* ```
270+
*
271+
* @memberof AsgardeoSPAClient
272+
*/
273+
public static getInstanceKeys(): number[] {
274+
return Array.from(this._instances.keys());
275+
}
209276

210-
return this._instances.get(0);
277+
/**
278+
* This method removes all instances.
279+
* Useful for cleanup in testing scenarios or application teardown.
280+
*
281+
* @example
282+
* ```
283+
* AsgardeoSPAClient.destroyAllInstances();
284+
* ```
285+
*
286+
* @memberof AsgardeoSPAClient
287+
*/
288+
public static destroyAllInstances(): void {
289+
// Clean up each instance's session data before clearing
290+
this._instances.forEach((instance) => {
291+
instance.clearSession();
292+
});
293+
this._instances.clear();
294+
}
295+
296+
/**
297+
* This method returns the instance ID for this client instance.
298+
*
299+
* @return {number} - The instance ID.
300+
*
301+
* @example
302+
* ```
303+
* const auth = AsgardeoSPAClient.getInstance(1);
304+
* console.log(auth.getInstanceId()); // 1
305+
* ```
306+
*
307+
* @memberof AsgardeoSPAClient
308+
*
309+
* @preserve
310+
*/
311+
public getInstanceId(): number {
312+
return this._instanceID;
211313
}
212314

213315
/**

packages/browser/src/utils/http.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,53 @@
2020
import {AsgardeoSPAClient} from '../__legacy__/client';
2121

2222
/**
23-
* HTTP utility for making requests using the AsgardeoSPAClient instance.
23+
* Creates an HTTP utility for making requests using a specific AsgardeoSPAClient instance.
24+
*
25+
* @param instanceId - Optional instance ID for multi-instance support. Defaults to 0.
26+
* @returns An object with request and requestAll methods bound to the specified instance.
2427
*
2528
* @remarks
26-
* This utility provides methods to make single or multiple HTTP requests.
29+
* This utility provides methods to make single or multiple HTTP requests for a specific instance.
30+
*
31+
* @example
32+
* ```typescript
33+
* // Use default instance
34+
* const httpClient = http();
35+
*
36+
* // Use specific instance
37+
* const httpInstance1 = http(1);
38+
* const httpInstance2 = http(2);
39+
* ```
2740
*/
28-
const http: {
41+
export const http = (
42+
instanceId: number = 0,
43+
): {
2944
request: typeof AsgardeoSPAClient.prototype.httpRequest;
3045
requestAll: typeof AsgardeoSPAClient.prototype.httpRequestAll;
31-
} = {
32-
/**
33-
* Makes a single HTTP request using the AsgardeoSPAClient instance.
34-
*
35-
* @param config - The HTTP request configuration object.
36-
* @returns A promise resolving to the HTTP response.
37-
*/
38-
request: AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()),
46+
} => {
47+
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
3948

40-
/**
41-
* Makes multiple HTTP requests in parallel using the AsgardeoSPAClient instance.
42-
*
43-
* @param configs - An array of HTTP request configuration objects.
44-
* @returns A promise resolving to an array of HTTP responses.
45-
*/
46-
requestAll: AsgardeoSPAClient.getInstance().httpRequestAll.bind(AsgardeoSPAClient.getInstance()),
49+
return {
50+
/**
51+
* Makes a single HTTP request using the AsgardeoSPAClient instance.
52+
*
53+
* @param config - The HTTP request configuration object.
54+
* @returns A promise resolving to the HTTP response.
55+
*/
56+
request: client.httpRequest.bind(client),
57+
58+
/**
59+
* Makes multiple HTTP requests in parallel using the AsgardeoSPAClient instance.
60+
*
61+
* @param configs - An array of HTTP request configuration objects.
62+
* @returns A promise resolving to an array of HTTP responses.
63+
*/
64+
requestAll: client.httpRequestAll.bind(client),
65+
};
4766
};
4867

49-
export default http;
68+
/**
69+
* Default HTTP utility using instance 0.
70+
* For multi-instance support, use http(instanceId) instead.
71+
*/
72+
export default http();

packages/javascript/src/__legacy__/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class AsgardeoAuthClient<T> {
127127
this.instanceIdValue += 1;
128128
}
129129

130-
if (instanceID) {
130+
if (instanceID !== undefined) {
131131
this.instanceIdValue = instanceID;
132132
}
133133

packages/react/src/AsgardeoReactClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
189189
baseUrl = configData?.baseUrl;
190190
}
191191

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

195195
const processedSchemas: any = flattenUserSchema(schemas);
196196

@@ -220,7 +220,7 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
220220
baseUrl = configData?.baseUrl;
221221
}
222222

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

245-
return await getAllOrganizations({baseUrl});
245+
return await getAllOrganizations({baseUrl, instanceId: this.getInstanceId()});
246246
} catch (error) {
247247
throw new AsgardeoRuntimeError(
248248
`Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`,

packages/react/src/api/createOrganization.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import {
2626
CreateOrganizationConfig as BaseCreateOrganizationConfig,
2727
} from '@asgardeo/browser';
2828

29-
const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());
30-
3129
/**
3230
* Configuration for the createOrganization request (React-specific)
3331
*/
@@ -37,6 +35,10 @@ export interface CreateOrganizationConfig extends Omit<BaseCreateOrganizationCon
3735
* which is a wrapper around axios http.request
3836
*/
3937
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
38+
/**
39+
* Optional instance ID for multi-instance support. Defaults to 0.
40+
*/
41+
instanceId?: number;
4042
}
4143

4244
/**
@@ -90,8 +92,14 @@ export interface CreateOrganizationConfig extends Omit<BaseCreateOrganizationCon
9092
* }
9193
* ```
9294
*/
93-
const createOrganization = async ({fetcher, ...requestConfig}: CreateOrganizationConfig): Promise<Organization> => {
95+
const createOrganization = async ({
96+
fetcher,
97+
instanceId = 0,
98+
...requestConfig
99+
}: CreateOrganizationConfig): Promise<Organization> => {
94100
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
101+
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
102+
const httpClient: HttpInstance = client.httpRequest.bind(client);
95103
const response: HttpResponse<any> = await httpClient({
96104
data: config.body ? JSON.parse(config.body as string) : undefined,
97105
headers: config.headers as Record<string, string>,

packages/react/src/api/getAllOrganizations.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import {
2626
AllOrganizationsApiResponse,
2727
} from '@asgardeo/browser';
2828

29-
const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());
30-
3129
/**
3230
* Configuration for the getAllOrganizations request (React-specific)
3331
*/
@@ -37,6 +35,10 @@ export interface GetAllOrganizationsConfig extends Omit<BaseGetAllOrganizationsC
3735
* which is a wrapper around axios http.request
3836
*/
3937
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
38+
/**
39+
* Optional instance ID for multi-instance support. Defaults to 0.
40+
*/
41+
instanceId?: number;
4042
}
4143

4244
/**
@@ -84,9 +86,12 @@ export interface GetAllOrganizationsConfig extends Omit<BaseGetAllOrganizationsC
8486
*/
8587
const getAllOrganizations = async ({
8688
fetcher,
89+
instanceId = 0,
8790
...requestConfig
8891
}: GetAllOrganizationsConfig): Promise<AllOrganizationsApiResponse> => {
8992
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
93+
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
94+
const httpClient: HttpInstance = client.httpRequest.bind(client);
9095
const response: HttpResponse<any> = await httpClient({
9196
headers: config.headers as Record<string, string>,
9297
method: config.method || 'GET',

packages/react/src/api/getMeOrganizations.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import {
2626
GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig,
2727
} from '@asgardeo/browser';
2828

29-
const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance());
30-
3129
/**
3230
* Configuration for the getMeOrganizations request (React-specific)
3331
*/
@@ -37,6 +35,10 @@ export interface GetMeOrganizationsConfig extends Omit<BaseGetMeOrganizationsCon
3735
* which is a wrapper around axios http.request
3836
*/
3937
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
38+
/**
39+
* Optional instance ID for multi-instance support. Defaults to 0.
40+
*/
41+
instanceId?: number;
4042
}
4143

4244
/**
@@ -86,8 +88,14 @@ export interface GetMeOrganizationsConfig extends Omit<BaseGetMeOrganizationsCon
8688
* }
8789
* ```
8890
*/
89-
const getMeOrganizations = async ({fetcher, ...requestConfig}: GetMeOrganizationsConfig): Promise<Organization[]> => {
91+
const getMeOrganizations = async ({
92+
fetcher,
93+
instanceId = 0,
94+
...requestConfig
95+
}: GetMeOrganizationsConfig): Promise<Organization[]> => {
9096
const defaultFetcher = async (url: string, config: RequestInit): Promise<Response> => {
97+
const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId);
98+
const httpClient: HttpInstance = client.httpRequest.bind(client);
9199
const response: HttpResponse<any> = await httpClient({
92100
headers: config.headers as Record<string, string>,
93101
method: config.method || 'GET',

0 commit comments

Comments
 (0)