Skip to content
Merged
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/afraid-buckets-jog.md
Original file line number Diff line number Diff line change
@@ -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
72 changes: 43 additions & 29 deletions packages/browser/src/__legacy__/helpers/authentication-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
OIDCEndpoints,
TokenResponse,
extractPkceStorageKeyFromState,
Config,
} from '@asgardeo/javascript';
import {SPAHelper} from './spa-helper';
import {
Expand Down Expand Up @@ -103,26 +104,31 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
config: SPACustomGrantConfig,
enableRetrievingSignOutURLFromSession?: (config: SPACustomGrantConfig) => void,
): Promise<User | Response> {
const _config: Config = (await this._storageManager.getConfigData()) as Config;
let useDefaultEndpoint = true;
let matches = false;

// If the config does not contains a token endpoint, default token endpoint will be used.
if (config?.tokenEndpoint) {
useDefaultEndpoint = false;

for (const baseUrl of [
...((await this._storageManager.getConfigData())?.resourceServerURLs ?? []),
(config as any).baseUrl,
]) {
if (baseUrl && config.tokenEndpoint?.startsWith(baseUrl)) {
matches = true;
break;
// Only validate URLs for WebWorker storage
if (_config.storage === BrowserStorage.WebWorker) {
for (const baseUrl of [...(_config?.allowedExternalUrls ?? []), (config as any).baseUrl]) {
if (baseUrl && config.tokenEndpoint?.startsWith(baseUrl)) {
matches = true;
break;
}
}
} else {
matches = true;
}
}

if (config.shouldReplayAfterRefresh) {
this._storageManager.setTemporaryDataParameter(CUSTOM_GRANT_CONFIG, JSON.stringify(config));
}

if (useDefaultEndpoint || matches) {
return this._authenticationClient
.exchangeToken(config)
Expand All @@ -147,9 +153,9 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
new AsgardeoAuthException(
'SPA-MAIN_THREAD_CLIENT-RCG-IV01',
'Request to the provided endpoint is prohibited.',
'Requests can only be sent to resource servers specified by the `resourceServerURLs`' +
'Requests can only be sent to resource servers specified by the `allowedExternalUrls`' +
' attribute while initializing the SDK. The specified token endpoint in this request ' +
'cannot be found among the `resourceServerURLs`',
'cannot be found among the `allowedExternalUrls`',
),
);
}
Expand Down Expand Up @@ -224,14 +230,19 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
enableRetrievingSignOutURLFromSession?: (config: SPACustomGrantConfig) => void,
): Promise<HttpResponse> {
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) {
Expand Down Expand Up @@ -319,9 +330,9 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
new AsgardeoAuthException(
'SPA-AUTH_HELPER-HR-IV02',
'Request to the provided endpoint is prohibited.',
'Requests can only be sent to resource servers specified by the `resourceServerURLs`' +
'Requests can only be sent to resource servers specified by the `allowedExternalUrls`' +
' attribute while initializing the SDK. The specified endpoint in this request ' +
'cannot be found among the `resourceServerURLs`',
'cannot be found among the `allowedExternalUrls`',
),
);
}
Expand All @@ -335,23 +346,26 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
httpFinishCallback?: () => void,
): Promise<HttpResponse[] | undefined> {
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;
}
}
}

Expand Down Expand Up @@ -436,9 +450,9 @@ export class AuthenticationHelper<T extends MainThreadClientConfig | WebWorkerCl
throw new AsgardeoAuthException(
'SPA-AUTH_HELPER-HRA-IV02',
'Request to the provided endpoint is prohibited.',
'Requests can only be sent to resource servers specified by the `resourceServerURLs`' +
'Requests can only be sent to resource servers specified by the `allowedExternalUrls`' +
' attribute while initializing the SDK. The specified endpoint in this request ' +
'cannot be found among the `resourceServerURLs`',
'cannot be found among the `allowedExternalUrls`',
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/__legacy__/models/client-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface SPAConfig {
syncSession?: boolean;
checkSessionInterval?: number;
sessionRefreshInterval?: number;
resourceServerURLs?: string[];
allowedExternalUrls?: string[];
authParams?: Record<string, string>;
periodicTokenRefresh?: boolean;
autoLogoutOnTokenRefreshError?: boolean;
Expand Down
18 changes: 18 additions & 0 deletions packages/javascript/src/models/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ export interface BaseConfig<T = unknown> 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.
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/contexts/Asgardeo/AsgardeoContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export type AsgardeoContextProps = {
*/
reInitialize: (config: Partial<AsgardeoReactConfig>) => Promise<boolean>;
} & Pick<AsgardeoReactConfig, 'storage' | 'platform'> &
Pick<AsgardeoReactClient, 'clearSession'>;
Pick<AsgardeoReactClient, 'clearSession' | 'switchOrganization'>;

/**
* Context object for managing the Authentication flow builder core context.
Expand Down Expand Up @@ -170,6 +170,7 @@ const AsgardeoContext: Context<AsgardeoContextProps | null> = createContext<null
getAccessToken: null,
exchangeToken: null,
storage: 'sessionStorage',
switchOrganization: null,
reInitialize: null,
platform: undefined,
});
Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
Platform,
extractUserClaimsFromIdToken,
EmbeddedSignInFlowResponseV2,
TokenResponse,
} from '@asgardeo/browser';
import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, useCallback} from 'react';
import AsgardeoContext from './AsgardeoContext';
Expand Down Expand Up @@ -459,15 +460,17 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
}
};

const switchOrganization = async (organization: Organization): Promise<void> => {
const switchOrganization = async (organization: Organization): Promise<TokenResponse | Response> => {
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))}`,
Expand Down Expand Up @@ -519,6 +522,7 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
exchangeToken: asgardeo.exchangeToken.bind(asgardeo),
syncSession,
platform: config?.platform,
switchOrganization,
}),
[
applicationId,
Expand All @@ -537,6 +541,7 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
asgardeo,
signInOptions,
syncSession,
switchOrganization,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -56,7 +57,7 @@ export interface OrganizationProviderProps {
/**
* Callback function called when switching organizations
*/
onOrganizationSwitch?: (organization: Organization) => Promise<void>;
onOrganizationSwitch?: (organization: Organization) => Promise<TokenResponse | Response>;
/**
* Refetch the my organizations list.
* @returns
Expand Down
Loading