diff --git a/.changeset/afraid-buckets-jog.md b/.changeset/afraid-buckets-jog.md new file mode 100644 index 00000000..7db36f5b --- /dev/null +++ b/.changeset/afraid-buckets-jog.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/browser': patch +'@asgardeo/react': patch +--- + +Add ability to call external endpoints without explicitly allowing them in non `webWorker` storage modes diff --git a/packages/browser/src/__legacy__/helpers/authentication-helper.ts b/packages/browser/src/__legacy__/helpers/authentication-helper.ts index d7b7dcc8..4309075d 100644 --- a/packages/browser/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/browser/src/__legacy__/helpers/authentication-helper.ts @@ -29,6 +29,7 @@ import { OIDCEndpoints, TokenResponse, extractPkceStorageKeyFromState, + Config, } from '@asgardeo/javascript'; import {SPAHelper} from './spa-helper'; import { @@ -103,6 +104,7 @@ export class AuthenticationHelper void, ): Promise { + const _config: Config = (await this._storageManager.getConfigData()) as Config; let useDefaultEndpoint = true; let matches = false; @@ -110,19 +112,23 @@ export class AuthenticationHelper void, ): Promise { let matches = false; - const config = await this._storageManager.getConfigData(); + const config: Config = (await this._storageManager.getConfigData()) as Config; - for (const baseUrl of [...((await config?.resourceServerURLs) ?? []), (config as any).baseUrl]) { - if (baseUrl && requestConfig?.url?.startsWith(baseUrl)) { - matches = true; + // Only validate URLs for WebWorker storage + if (config.storage === BrowserStorage.WebWorker) { + for (const baseUrl of [...(config?.allowedExternalUrls ?? []), (config as any).baseUrl]) { + if (baseUrl && requestConfig?.url?.startsWith(baseUrl)) { + matches = true; - break; + break; + } } + } else { + matches = true; } if (matches) { @@ -319,9 +330,9 @@ export class AuthenticationHelper void, ): Promise { let matches = true; - const config = await this._storageManager.getConfigData(); + const config: Config = (await this._storageManager.getConfigData()) as Config; - for (const requestConfig of requestConfigs) { - let urlMatches = false; + // Only validate URLs for WebWorker storage + if (config.storage === BrowserStorage.WebWorker) { + for (const requestConfig of requestConfigs) { + let urlMatches = false; - for (const baseUrl of [...((await config)?.resourceServerURLs ?? []), (config as any).baseUrl]) { - if (baseUrl && requestConfig.url?.startsWith(baseUrl)) { - urlMatches = true; + for (const baseUrl of [...(config?.allowedExternalUrls ?? []), (config as any).baseUrl]) { + if (baseUrl && requestConfig.url?.startsWith(baseUrl)) { + urlMatches = true; - break; + break; + } } - } - if (!urlMatches) { - matches = false; + if (!urlMatches) { + matches = false; - break; + break; + } } } @@ -436,9 +450,9 @@ export class AuthenticationHelper; periodicTokenRefresh?: boolean; autoLogoutOnTokenRefreshError?: boolean; diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 659dab1b..b2f88891 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -78,6 +78,24 @@ export interface BaseConfig extends WithPreferences { */ afterSignOutUrl?: string | undefined; + /** + * A list of external API base URLs that the SDK is allowed to attach access tokens to when making HTTP requests. + * + * When making authenticated HTTP requests using the SDK's HTTP client, the access token will only be attached + * to requests whose URLs start with one of these specified base URLs. This provides a security layer by + * preventing tokens from being sent to unauthorized servers. + * + * @remarks + * - This is only applicable when the storage type is `webWorker`. + * - Each URL should be a base URL without trailing slashes (e.g., "https://api.example.com"). + * - The SDK will check if the request URL starts with any of these base URLs before attaching the token. + * - If a request is made to a URL that doesn't match any of these base URLs, an error will be thrown. + * + * @example + * allowedExternalUrls: ["https://api.example.com", "https://api.another-service.com"] + */ + allowedExternalUrls?: 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/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index f705f3cc..03a40f7a 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -139,7 +139,7 @@ export type AsgardeoContextProps = { */ reInitialize: (config: Partial) => Promise; } & Pick & - Pick; + Pick; /** * Context object for managing the Authentication flow builder core context. @@ -170,6 +170,7 @@ const AsgardeoContext: Context = createContext> = ({ } }; - const switchOrganization = async (organization: Organization): Promise => { + const switchOrganization = async (organization: Organization): Promise => { try { setIsUpdatingSession(true); setIsLoadingSync(true); - await asgardeo.switchOrganization(organization); + const response: TokenResponse | Response = await asgardeo.switchOrganization(organization); if (await asgardeo.isSignedIn()) { await updateSession(); } + + return response; } catch (error) { throw new AsgardeoRuntimeError( `Failed to switch organization: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`, @@ -519,6 +522,7 @@ const AsgardeoProvider: FC> = ({ exchangeToken: asgardeo.exchangeToken.bind(asgardeo), syncSession, platform: config?.platform, + switchOrganization, }), [ applicationId, @@ -537,6 +541,7 @@ const AsgardeoProvider: FC> = ({ asgardeo, signInOptions, syncSession, + switchOrganization, ], ); diff --git a/packages/react/src/contexts/Organization/OrganizationProvider.tsx b/packages/react/src/contexts/Organization/OrganizationProvider.tsx index 960bf4f6..91af4661 100644 --- a/packages/react/src/contexts/Organization/OrganizationProvider.tsx +++ b/packages/react/src/contexts/Organization/OrganizationProvider.tsx @@ -21,6 +21,7 @@ import { Organization, AllOrganizationsApiResponse, CreateOrganizationPayload, + TokenResponse, } from '@asgardeo/browser'; import {FC, PropsWithChildren, ReactElement, useCallback, useMemo, useState} from 'react'; import OrganizationContext, {OrganizationContextProps} from './OrganizationContext'; @@ -56,7 +57,7 @@ export interface OrganizationProviderProps { /** * Callback function called when switching organizations */ - onOrganizationSwitch?: (organization: Organization) => Promise; + onOrganizationSwitch?: (organization: Organization) => Promise; /** * Refetch the my organizations list. * @returns