diff --git a/packages/browser/src/__legacy__/client.ts b/packages/browser/src/__legacy__/client.ts index 74359341a..06f0a88fe 100755 --- a/packages/browser/src/__legacy__/client.ts +++ b/packages/browser/src/__legacy__/client.ts @@ -87,10 +87,10 @@ export class AsgardeoSPAClient { protected _onEndUserSession: (response: any) => void = () => null; protected _onInitialize: (response: boolean) => void = () => null; protected _onCustomGrant: Map void> = new Map(); - protected _instanceID: number; + protected _instanceId: number; protected constructor(id: number) { - this._instanceID = id; + this._instanceId = id; } public instantiateAuthHelper(authHelper?: typeof AuthenticationHelper): void { @@ -309,7 +309,7 @@ export class AsgardeoSPAClient { * @preserve */ public getInstanceId(): number { - return this._instanceID; + return this._instanceId; } /** @@ -364,7 +364,7 @@ export class AsgardeoSPAClient { // Tracker: https://github.com/asgardeo/asgardeo-auth-react-sdk/issues/240 if (!this._client || (this._client && (!_config || Object.keys(_config)?.length === 0))) { this._client = await MainThreadClient( - this._instanceID, + this._instanceId, mergedConfig, (authClient: AsgardeoAuthClient, spaHelper: SPAHelper) => { return new this._authHelper(authClient, spaHelper); @@ -399,7 +399,7 @@ export class AsgardeoSPAClient { if (!this._client || (this._client && (!_config || Object.keys(_config)?.length === 0))) { const webWorkerClientConfig = config as AuthClientConfig; this._client = (await WebWorkerClient( - this._instanceID, + this._instanceId, { ...DefaultConfig, ...webWorkerClientConfig, @@ -718,8 +718,6 @@ export class AsgardeoSPAClient { public async exchangeToken(config: TokenExchangeRequestConfig): Promise { if (config.signInRequired) { await this._validateMethod(); - } else { - await this._validateMethod(); } if (!config.id) { diff --git a/packages/browser/src/__legacy__/clients/main-thread-client.ts b/packages/browser/src/__legacy__/clients/main-thread-client.ts index 79900c12b..8802c2229 100755 --- a/packages/browser/src/__legacy__/clients/main-thread-client.ts +++ b/packages/browser/src/__legacy__/clients/main-thread-client.ts @@ -57,7 +57,7 @@ const initiateStore = (store: BrowserStorage | undefined): Storage => { }; export const MainThreadClient = async ( - instanceID: number, + instanceId: number, config: AuthClientConfig, getAuthHelper: ( authClient: AsgardeoAuthClient, @@ -67,7 +67,7 @@ export const MainThreadClient = async ( const _store: Storage = initiateStore(config.storage as BrowserStorage); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); - await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID); + await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceId); const _spaHelper = new SPAHelper(_authenticationClient); const _dataLayer = _authenticationClient.getStorageManager(); @@ -85,7 +85,7 @@ export const MainThreadClient = async ( let _getSignOutURLFromSessionStorage: boolean = false; - const _httpClient: HttpClientInstance = HttpClient.getInstance(instanceID); + const _httpClient: HttpClientInstance = HttpClient.getInstance(instanceId); let _isHttpHandlerEnabled: boolean = true; let _httpErrorCallback: (error: HttpError) => void | Promise; let _httpFinishCallback: () => void; @@ -261,7 +261,7 @@ export const MainThreadClient = async ( if ((await _authenticationClient.isSignedIn()) && !_getSignOutURLFromSessionStorage) { location.href = await _authenticationClient.getSignOutUrl(); } else { - location.href = SPAUtils.getSignOutUrl(config.clientId, instanceID); + location.href = SPAUtils.getSignOutUrl(config.clientId, instanceId); } _spaHelper.clearRefreshTokenTimeout(); diff --git a/packages/browser/src/__legacy__/clients/web-worker-client.ts b/packages/browser/src/__legacy__/clients/web-worker-client.ts index 0ce4518d1..ad981ec9d 100755 --- a/packages/browser/src/__legacy__/clients/web-worker-client.ts +++ b/packages/browser/src/__legacy__/clients/web-worker-client.ts @@ -92,7 +92,7 @@ const initiateStore = (store: BrowserStorage | undefined): Storage => { }; export const WebWorkerClient = async ( - instanceID: number, + instanceId: number, config: AuthClientConfig, webWorker: new () => Worker, getAuthHelper: ( @@ -114,7 +114,7 @@ export const WebWorkerClient = async ( const _store: Storage = initiateStore(config.storage as BrowserStorage); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); - await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID); + await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceId); const _spaHelper = new SPAHelper(_authenticationClient); const _sessionManagementHelper = await SessionManagementHelper( @@ -128,7 +128,7 @@ export const WebWorkerClient = async ( return signOutURL; } catch { - return SPAUtils.getSignOutUrl(config.clientId, instanceID); + return SPAUtils.getSignOutUrl(config.clientId, instanceId); } }, config.storage as BrowserStorage, @@ -365,8 +365,9 @@ export const WebWorkerClient = async ( } }; - const message: Message & {instanceID: number}> = { - data: {...config, instanceID}, + const message: Message> = { + data: config, + instanceId, type: INIT, }; @@ -524,7 +525,7 @@ export const WebWorkerClient = async ( return communicate(message) .then((url: string) => { - SPAUtils.setSignOutURL(url, config.clientId, instanceID); + SPAUtils.setSignOutURL(url, config.clientId, instanceId); // Enable OIDC Sessions Management only if it is set to true in the config. if (config.syncSession) { @@ -656,7 +657,7 @@ export const WebWorkerClient = async ( return reject(error); }); } else { - window.location.href = SPAUtils.getSignOutUrl(config.clientId, instanceID); + window.location.href = SPAUtils.getSignOutUrl(config.clientId, instanceId); return SPAUtils.waitTillPageRedirect().then(() => { return Promise.resolve(true); diff --git a/packages/browser/src/__legacy__/helpers/authentication-helper.ts b/packages/browser/src/__legacy__/helpers/authentication-helper.ts index 4309075d7..7511dda1b 100644 --- a/packages/browser/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/browser/src/__legacy__/helpers/authentication-helper.ts @@ -63,14 +63,14 @@ export class AuthenticationHelper; protected _storageManager: StorageManager; protected _spaHelper: SPAHelper; - protected _instanceID: number; + protected _instanceId: number; protected _isTokenRefreshing: boolean; public constructor(authClient: AsgardeoAuthClient, spaHelper: SPAHelper) { this._authenticationClient = authClient; this._storageManager = this._authenticationClient.getStorageManager(); this._spaHelper = spaHelper; - this._instanceID = this._authenticationClient.getInstanceId(); + this._instanceId = this._authenticationClient.getInstanceId(); this._isTokenRefreshing = false; } @@ -486,7 +486,7 @@ export class AuthenticationHelper { export interface Message { type: MessageType; data?: T; + instanceId?: number; } export interface AuthorizationInfo { diff --git a/packages/browser/src/__legacy__/utils/spa-utils.ts b/packages/browser/src/__legacy__/utils/spa-utils.ts index bf35ff957..7122a09cc 100644 --- a/packages/browser/src/__legacy__/utils/spa-utils.ts +++ b/packages/browser/src/__legacy__/utils/spa-utils.ts @@ -45,17 +45,17 @@ export class SPAUtils { sessionStorage.setItem(pkceKey, pkce); } - public static setSignOutURL(url: string, clientId: string, instanceID: number): void { + public static setSignOutURL(url: string, clientId: string, instanceId: number): void { sessionStorage.setItem( - `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientId}`, + `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceId}-${clientId}`, url, ); } - public static getSignOutUrl(clientId: string, instanceID: number): string { + public static getSignOutUrl(clientId: string, instanceId: number): string { return ( sessionStorage.getItem( - `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientId}`, + `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceId}-${clientId}`, ) ?? '' ); } diff --git a/packages/browser/src/__legacy__/worker/worker-core.ts b/packages/browser/src/__legacy__/worker/worker-core.ts index 6fc518a46..047b82053 100755 --- a/packages/browser/src/__legacy__/worker/worker-core.ts +++ b/packages/browser/src/__legacy__/worker/worker-core.ts @@ -42,7 +42,7 @@ import {MemoryStore} from '../stores'; import {SPACryptoUtils} from '../utils/crypto-utils'; export const WebWorkerCore = async ( - instanceID: number, + instanceId: number, config: AuthClientConfig, getAuthHelper: ( authClient: AsgardeoAuthClient, @@ -52,7 +52,7 @@ export const WebWorkerCore = async ( const _store: Storage = new MemoryStore(); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); - await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID); + await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceId); const _spaHelper = new SPAHelper(_authenticationClient); @@ -63,7 +63,7 @@ export const WebWorkerCore = async ( const _dataLayer = _authenticationClient.getStorageManager(); - const _httpClient: HttpClientInstance = HttpClient.getInstance(instanceID); + const _httpClient: HttpClientInstance = HttpClient.getInstance(instanceId); const attachToken = async (request: HttpRequestConfig): Promise => { await _authenticationHelper.attachTokenToRequestConfig(request); diff --git a/packages/browser/src/__legacy__/worker/worker-receiver.ts b/packages/browser/src/__legacy__/worker/worker-receiver.ts index ac75e7e78..074e815ce 100644 --- a/packages/browser/src/__legacy__/worker/worker-receiver.ts +++ b/packages/browser/src/__legacy__/worker/worker-receiver.ts @@ -84,9 +84,9 @@ export const workerReceiver = ( switch (data.type) { case INIT: try { - const {instanceID = 0, ...configData} = data.data; - const config: AuthClientConfig = {...configData}; - webWorker = await WebWorkerCore(instanceID, config, getAuthHelper); + const instanceId: number = data.instanceId ?? 0; + const config: AuthClientConfig = {...data.data}; + webWorker = await WebWorkerCore(instanceId, config, getAuthHelper); webWorker.setHttpRequestFinishCallback(onRequestFinishCallback); webWorker.setHttpRequestStartCallback(onRequestStartCallback); webWorker.setHttpRequestSuccessCallback(onRequestSuccessCallback); diff --git a/packages/javascript/src/StorageManager.ts b/packages/javascript/src/StorageManager.ts index c955b2efb..d27d353c8 100644 --- a/packages/javascript/src/StorageManager.ts +++ b/packages/javascript/src/StorageManager.ts @@ -75,8 +75,17 @@ class StorageManager { await this.store.setData(key, dataToBeSavedJSON); } - protected resolveKey(store: Stores | string, userId?: string): string { - return userId ? `${store}-${this.id}-${userId}` : `${store}-${this.id}`; + protected resolveKey(store: Stores | string, userId?: string, instanceId?: string): string { + if (userId && instanceId) { + return `${store}-${instanceId}-${userId}`; + } + if (userId) { + return `${store}-${this.id}-${userId}`; + } + if (instanceId) { + return `${store}-${instanceId}`; + } + return `${store}-${this.id}`; } protected static isLocalStorageAvailable(): boolean { @@ -124,8 +133,8 @@ class StorageManager { return JSON.parse((await this.store.getData(this.resolveKey(Stores.TemporaryData, userId))) ?? null); } - public async getSessionData(userId?: string): Promise { - return JSON.parse((await this.store.getData(this.resolveKey(Stores.SessionData, userId))) ?? null); + public async getSessionData(userId?: string, instanceId?: string): Promise { + return JSON.parse((await this.store.getData(this.resolveKey(Stores.SessionData, userId, instanceId))) ?? null); } public async getCustomData(key: string, userId?: string): Promise { diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index 043829737..d96b4b3e2 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -914,77 +914,89 @@ export class AsgardeoAuthClient { * @preserve */ public async exchangeToken(config: TokenExchangeRequestConfig, userId?: string): Promise { - const oidcProviderMetadata: OIDCDiscoveryApiResponse = await this.oidcProviderMetaDataProvider(); - const configData: StrictAuthClientConfig = await this.configProvider(); + const executeTokenExchange = async (): Promise => { + const oidcProviderMetadata: OIDCDiscoveryApiResponse = await this.oidcProviderMetaDataProvider(); + const configData: StrictAuthClientConfig = await this.configProvider(); - let tokenEndpoint: string | undefined; + let tokenEndpoint: string | undefined; - if (config.tokenEndpoint && config.tokenEndpoint.trim().length !== 0) { - tokenEndpoint = config.tokenEndpoint; - } else { - tokenEndpoint = oidcProviderMetadata.token_endpoint; - } + if (config.tokenEndpoint && config.tokenEndpoint.trim().length !== 0) { + tokenEndpoint = config.tokenEndpoint; + } else { + tokenEndpoint = oidcProviderMetadata.token_endpoint; + } - if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-NF01', - 'Token endpoint not found.', - 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + - 'or the token endpoint passed to the SDK is empty.', - ); - } + if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-NF01', + 'Token endpoint not found.', + 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + + 'or the token endpoint passed to the SDK is empty.', + ); + } - const data: string[] = await Promise.all( - Object.entries(config.data).map(async ([key, value]: [key: string, value: any]) => { - const newValue: string = await this.authHelper.replaceCustomGrantTemplateTags(value as string, userId); + const data: string[] = await Promise.all( + Object.entries(config.data).map(async ([key, value]: [key: string, value: any]) => { + const newValue: string = await this.authHelper.replaceCustomGrantTemplateTags(value as string, userId); - return `${key}=${newValue}`; - }), - ); + return `${key}=${newValue}`; + }), + ); - let requestHeaders: Record = { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }; + let requestHeaders: Record = { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }; - if (config.attachToken) { - requestHeaders = { - ...requestHeaders, - Authorization: `Bearer ${(await this.storageManager.getSessionData(userId)).access_token}`, + if (config.attachToken) { + requestHeaders = { + ...requestHeaders, + Authorization: `Bearer ${(await this.storageManager.getSessionData(userId)).access_token}`, + }; + } + + const requestConfig: RequestInit = { + body: data.join('&'), + credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', + headers: new Headers(requestHeaders), + method: 'POST', }; - } - const requestConfig: RequestInit = { - body: data.join('&'), - credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', - headers: new Headers(requestHeaders), - method: 'POST', - }; + let response: Response; - let response: Response; + try { + response = await fetch(tokenEndpoint, requestConfig); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-NE02', + 'The custom grant request failed.', + error ?? 'The request sent to get the custom grant failed.', + ); + } - try { - response = await fetch(tokenEndpoint, requestConfig); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-NE02', - 'The custom grant request failed.', - error ?? 'The request sent to get the custom grant failed.', - ); - } + if (response.status !== 200 || !response.ok) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-HE03', + `Invalid response status received for the custom grant request. (${response.statusText})`, + (await response.json()) as string, + ); + } - if (response.status !== 200 || !response.ok) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-HE03', - `Invalid response status received for the custom grant request. (${response.statusText})`, - (await response.json()) as string, - ); - } + if (config.returnsSession) { + return this.authHelper.handleTokenResponse(response, userId); + } + return Promise.resolve((await response.json()) as TokenResponse | Response); + }; - if (config.returnsSession) { - return this.authHelper.handleTokenResponse(response, userId); + if ( + await this.storageManager.getTemporaryDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, + ) + ) { + return executeTokenExchange(); } - return Promise.resolve((await response.json()) as TokenResponse | Response); + + return this.loadOpenIDProviderConfiguration(false).then(() => executeTokenExchange()); } /** diff --git a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts index 4553b9011..e0ca352f1 100644 --- a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts @@ -238,7 +238,31 @@ export class AuthenticationHelper { public async replaceCustomGrantTemplateTags(text: string, userId?: string): Promise { const configData: StrictAuthClientConfig = await this.config(); - const sessionData: SessionData = await this.storageManager.getSessionData(userId); + + const sourceInstanceId: string | number | null = configData.organizationChain?.sourceInstanceId ?? null; + + let sessionData: SessionData; + + if (sourceInstanceId) { + const {clientId} = configData; + let instanceKey: string; + if (clientId) { + instanceKey = `instance_${sourceInstanceId}-${clientId}`; + } else { + instanceKey = `instance_${sourceInstanceId}`; + } + sessionData = await this.storageManager.getSessionData(userId, instanceKey); + + if (!sessionData.access_token) { + throw new AsgardeoAuthException( + 'JS-AUTH_HELPER-RCGTT-NE01', + 'No session data found for source instance.', + 'Failed to retrieve session data from the source organization context.', + ); + } + } else { + sessionData = await this.storageManager.getSessionData(userId); + } const scope: string = processOpenIDScopes(configData.scopes); diff --git a/packages/javascript/src/__legacy__/models/client-config.ts b/packages/javascript/src/__legacy__/models/client-config.ts index 6a5c94b7c..636e41756 100644 --- a/packages/javascript/src/__legacy__/models/client-config.ts +++ b/packages/javascript/src/__legacy__/models/client-config.ts @@ -26,6 +26,18 @@ export interface DefaultAuthClientConfig { clientId?: string; clientSecret?: string; enablePKCE?: boolean; + organizationChain?: { + /** + * Instance ID of the source organization context to retrieve access token from for organization token exchange. + * Used in linked organization scenarios to automatically fetch the source organization's access token. + */ + sourceInstanceId?: string | number; + /** + * Organization ID for the target organization. + * When provided with sourceInstanceId, triggers automatic organization token exchange. + */ + targetOrganizationId?: string; + }; prompt?: string; responseMode?: OAuthResponseMode; scopes?: string | string[] | undefined; diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 075a13510..c95babc8a 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -129,6 +129,24 @@ export interface BaseConfig extends WithPreferences { */ instanceId?: number; + /** + * Configuration for chaining authentication across multiple organization contexts. + * Used when you need to authenticate a user in one organization using credentials + * from another organization context. + */ + organizationChain?: { + /** + * Instance ID of the source organization context to retrieve access token from for organization token exchange. + * Used in linked organization scenarios to automatically fetch the source organization's access token. + */ + sourceInstanceId?: number; + /** + * Organization ID for the target organization. + * When provided with sourceInstanceId, triggers automatic organization token exchange. + */ + targetOrganizationId?: string; + }; + /** * Optional organization handle for the Organization in Asgardeo. * This is used to identify the organization in the Asgardeo identity server in cases like Branding, etc. diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index eb3d2884d..e9655beec 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -276,6 +276,9 @@ class AsgardeoReactClient e override async switchOrganization(organization: Organization): Promise { return this.withLoading(async () => { try { + const configData: any = await this.asgardeo.getConfigData(); + const sourceInstanceId: number | undefined = configData?.organizationChain?.sourceInstanceId; + if (!organization.id) { throw new AsgardeoRuntimeError( 'Organization ID is required for switching organizations', @@ -296,7 +299,7 @@ class AsgardeoReactClient e }, id: 'organization-switch', returnsSession: true, - signInRequired: true, + signInRequired: sourceInstanceId === undefined, }; return (await this.asgardeo.exchangeToken(exchangeConfig, () => {})) as TokenResponse | Response; diff --git a/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx b/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx new file mode 100644 index 000000000..890d1879a --- /dev/null +++ b/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx @@ -0,0 +1,84 @@ +/** + * 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. + */ + +import {FC, PropsWithChildren} from 'react'; +import OrganizationContextController from './OrganizationContextController'; +import AsgardeoProvider, {AsgardeoProviderProps} from '../../../contexts/Asgardeo/AsgardeoProvider'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +export interface OrganizationContextProps extends Omit { + /** + * Optional base URL for the organization context. If not provided, it will default to the source provider's base URL. + */ + baseUrl?: string; + /** + * Instance ID for this organization context. Must be unique across the app if multiple contexts are used. + */ + instanceId: number; + /** + * Optional source instance ID. If not provided, immediate parent provider is used as source. + */ + sourceInstanceId?: number; + /** + * ID of the organization to authenticate with + */ + targetOrganizationId: string; +} + +const OrganizationContext: FC> = ({ + instanceId, + baseUrl, + clientId, + afterSignInUrl, + afterSignOutUrl, + targetOrganizationId, + sourceInstanceId, + scopes, + children, + ...rest +}: PropsWithChildren) => { + // Get the source provider's signed-in status + const { + isSignedIn: isSourceSignedIn, + instanceId: sourceInstanceIdFromContext, + baseUrl: sourceBaseUrl, + clientId: sourceClientId, + } = useAsgardeo(); + + return ( + + + {children} + + + ); +}; + +export default OrganizationContext; diff --git a/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx b/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx new file mode 100644 index 000000000..c2a4cdb90 --- /dev/null +++ b/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx @@ -0,0 +1,106 @@ +/** + * 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. + */ + +import {Organization} from '@asgardeo/browser'; +import {FC, useEffect, useRef} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +interface OrganizationContextControllerProps { + /** + * Children to render + */ + children: React.ReactNode; + /** + * Whether the source provider is signed in + */ + isSourceSignedIn: boolean; + /** + * ID of the organization to authenticate with + */ + targetOrganizationId: string; +} + +const OrganizationContextController: FC = ({ + targetOrganizationId, + isSourceSignedIn, + children, +}: OrganizationContextControllerProps) => { + const {isInitialized, isSignedIn, switchOrganization, isLoading} = useAsgardeo(); + const hasAuthenticatedRef: React.MutableRefObject = useRef(false); + const isAuthenticatingRef: React.MutableRefObject = useRef(false); + + /** + * Handle the organization switch when: + * - Current instance is initialized and NOT signed in + * - Source provider IS signed in + * Uses the `switchOrganization` function from the Asgardeo context. + */ + useEffect(() => { + const performOrganizationSwitch = async (): Promise => { + // Prevent multiple authentication attempts + if (hasAuthenticatedRef.current || isAuthenticatingRef.current) { + return; + } + + // Wait for initialization to complete + if (!isInitialized || isLoading) { + return; + } + + // Only proceed if user is not already signed in to this instance + if (isSignedIn) { + hasAuthenticatedRef.current = true; + return; + } + + // CRITICAL: Only proceed if source provider is signed in + if (!isSourceSignedIn) { + return; + } + + try { + isAuthenticatingRef.current = true; + hasAuthenticatedRef.current = true; + + // Build the organization object for authentication + const targetOrganization: Organization = { + id: targetOrganizationId, + name: '', // Name will be populated after authentication + orgHandle: '', // Will be populated after authentication + }; + + // Call the switchOrganization API from context (handles token exchange) + await switchOrganization(targetOrganization); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Linked organization authentication failed:', error); + + // Reset the flag to allow retry + hasAuthenticatedRef.current = false; + } finally { + isAuthenticatingRef.current = false; + } + }; + + performOrganizationSwitch(); + }, [isInitialized, isSignedIn, isLoading, isSourceSignedIn, targetOrganizationId, switchOrganization]); + + return <>{children}; +}; + +export default OrganizationContextController; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index 1989b9d5f..20559e2c1 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -37,6 +37,7 @@ export type AsgardeoContextProps = { afterSignInUrl: string | undefined; applicationId: string | undefined; baseUrl: string | undefined; + clientId: string | undefined; /** * Swaps the current access token with a new one based on the provided configuration (with a grant type). * @param config - Configuration for the token exchange request. @@ -169,6 +170,7 @@ const AsgardeoContext: Context = createContext {}, + clientId: undefined, exchangeToken: null, getAccessToken: null, getDecodedIdToken: null, diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 6584d6dca..73f24e92f 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -67,6 +67,7 @@ const AsgardeoProvider: FC> = ({ signInOptions, syncSession, instanceId = 0, + organizationChain, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); @@ -88,6 +89,7 @@ const AsgardeoProvider: FC> = ({ applicationId, baseUrl, clientId, + organizationChain, organizationHandle, scopes, signInOptions, @@ -563,6 +565,7 @@ const AsgardeoProvider: FC> = ({ applicationId, baseUrl, clearSession, + clientId, exchangeToken, getAccessToken, getDecodedIdToken, @@ -576,6 +579,7 @@ const AsgardeoProvider: FC> = ({ isLoading: isLoadingSync, isSignedIn: isSignedInSync, organization: currentOrganization, + organizationChain, organizationHandle: config?.organizationHandle, platform: config?.platform, reInitialize, @@ -597,6 +601,7 @@ const AsgardeoProvider: FC> = ({ signUpUrl, afterSignInUrl, baseUrl, + clientId, isInitializedSync, isLoadingSync, isSignedInSync, @@ -618,6 +623,7 @@ const AsgardeoProvider: FC> = ({ clearSession, reInitialize, instanceId, + organizationChain, ], ); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 4c51b7ac0..e2930f71e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -32,9 +32,6 @@ export * from './contexts/User/UserProvider'; export {default as useUser} from './contexts/User/useUser'; -export {default as OrganizationContext} from './contexts/Organization/OrganizationContext'; -export * from './contexts/Organization/OrganizationContext'; - export {default as OrganizationProvider} from './contexts/Organization/OrganizationProvider'; export * from './contexts/Organization/OrganizationProvider'; @@ -109,6 +106,9 @@ export * from './components/control/SignedOut'; export {default as Loading} from './components/control/Loading'; export * from './components/control/Loading'; +export {default as OrganizationContext} from './components/control/OrganizationContext/OrganizationContext'; +export * from './components/control/OrganizationContext/OrganizationContext'; + export {default as BaseSignIn} from './components/presentation/auth/SignIn/BaseSignIn'; export * from './components/presentation/auth/SignIn/BaseSignIn';