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
128 changes: 114 additions & 14 deletions packages/browser/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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 @@ -170,15 +172,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 @@ -187,22 +196,113 @@ 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)!;
}

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
*
* @preserve
*/
public static hasInstance(id: number = 0): boolean {
return this._instances.has(id);
}

this._instances.set(0, new AsgardeoSPAClient(0));
/**
* 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
*
* @preserve
*/
public static destroyInstance(id: number = 0): boolean {
return this._instances.delete(id);
}

return this._instances.get(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
*
* @preserve
*/
public static getInstanceKeys(): number[] {
return Array.from(this._instances.keys());
}

/**
* This method removes all instances.
* Useful for cleanup in testing scenarios or application teardown.
*
* @example
* ```
* AsgardeoSPAClient.destroyAllInstances();
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public static destroyAllInstances(): void {
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
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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;
20 changes: 11 additions & 9 deletions packages/javascript/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class AsgardeoAuthClient<T> {
private _cryptoUtils: Crypto;
private _cryptoHelper: IsomorphicCrypto;

private static _instanceID: number;
private _instanceID: number;

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

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

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

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

this._cryptoUtils = cryptoUtils;
Expand Down Expand Up @@ -182,7 +182,7 @@ export class AsgardeoAuthClient<T> {
* @preserve
*/
public getInstanceId(): number {
return AsgardeoAuthClient._instanceID;
return this._instanceID;
}

/**
Expand Down Expand Up @@ -245,6 +245,8 @@ export class AsgardeoAuthClient<T> {
authRequestConfig['client_secret'] = configData.clientSecret;
}

authRequestConfig['state'] = 'instance_' + this.getInstanceId() + '-' + configData.clientId;

const authorizeRequestParams: Map<string, string> = getAuthorizeRequestUrlParams(
{
redirectUri: configData.afterSignInUrl,
Expand Down
26 changes: 20 additions & 6 deletions packages/react/src/AsgardeoReactClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,26 @@ import getAllOrganizations from './api/getAllOrganizations';
class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> extends AsgardeoBrowserClient<T> {
private asgardeo: AuthAPI;
private _isLoading: boolean = false;
private _instanceId: number;

constructor() {
/**
* Creates a new AsgardeoReactClient instance.
* @param instanceId - Optional instance ID for multi-auth context support. Defaults to 0 for backward compatibility.
*/
constructor(instanceId: number = 0) {
super();
this._instanceId = instanceId;

// FIXME: This has to be the browser client from `@asgardeo/browser` package.
this.asgardeo = new AuthAPI();
this.asgardeo = new AuthAPI(undefined, instanceId);
}

/**
* Get the instance ID for this client.
* @returns The instance ID used for multi-auth context support.
*/
public getInstanceId(): number {
return this._instanceId;
}

/**
Expand Down Expand Up @@ -168,8 +182,8 @@ class AsgardeoReactClient<T extends AsgardeoReactConfig = AsgardeoReactConfig> e
baseUrl = configData?.baseUrl;
}

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

const processedSchemas = flattenUserSchema(schemas);

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

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

return getAllOrganizations({baseUrl});
return getAllOrganizations({baseUrl, instanceId: this._instanceId});
} catch (error) {
throw new AsgardeoRuntimeError(
`Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`,
Expand Down
14 changes: 12 additions & 2 deletions packages/react/src/__temp__/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ class AuthAPI {

private _authState = AuthAPI.DEFAULT_STATE;
private _client: AsgardeoSPAClient;
private _instanceId: number;

private _isLoading: boolean;

constructor(spaClient?: AsgardeoSPAClient) {
this._client = spaClient ?? AsgardeoSPAClient.getInstance();
constructor(spaClient?: AsgardeoSPAClient, instanceId: number = 0) {
this._instanceId = instanceId;
this._client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId);

this.getState = this.getState.bind(this);
this.init = this.init.bind(this);
Expand All @@ -50,6 +52,14 @@ class AuthAPI {
this.updateState = this.updateState.bind(this);
}

/**
* Get the instance ID for this AuthAPI instance.
* @returns The instance ID used for multi-auth context support.
*/
public getInstanceId(): number {
return this._instanceId;
}

public _setIsLoading(isLoading: boolean): void {
this._isLoading = isLoading;
}
Expand Down
11 changes: 8 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 {
CreateOrganizationPayload,
} 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,11 @@ 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 httpClient: HttpInstance = AsgardeoSPAClient.getInstance(instanceId).httpRequest.bind(
AsgardeoSPAClient.getInstance(instanceId)
);
const response = await httpClient({
url,
method: config.method || 'POST',
Expand Down
Loading
Loading