Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
311fc3f
Add a bakground ssoSilent call after interactive calls, with telemetry
sameerag Jan 19, 2026
632c393
update the interactive APIs
sameerag Jan 19, 2026
0a2f5e1
Change files
sameerag Jan 19, 2026
eb8df76
Update change/@azure-msal-common-461989e8-10b5-48f1-91dd-836248c3fce6…
sameerag Jan 19, 2026
ef600d9
Update change/@azure-msal-browser-440d27b3-88ff-459d-883e-a675b66d797…
sameerag Jan 19, 2026
fa1c809
Background SSO calls (#8253)
Copilot Jan 19, 2026
b0eac1a
Update lib/msal-browser/src/controllers/StandardController.ts
sameerag Jan 19, 2026
4565ab7
Update lib/msal-browser/src/config/Configuration.ts
sameerag Jan 19, 2026
f1fff9d
Update change/@azure-msal-browser-440d27b3-88ff-459d-883e-a675b66d797…
sameerag Jan 19, 2026
b14dbe8
Update change/@azure-msal-common-461989e8-10b5-48f1-91dd-836248c3fce6…
sameerag Jan 19, 2026
7626e80
Limit the sso to session refresh
sameerag Jan 19, 2026
e9a32f7
update apiExtractor
sameerag Jan 20, 2026
7f283d7
Update lib/msal-browser/src/config/Configuration.ts
sameerag Jan 20, 2026
64de352
Update lib/msal-browser/src/config/Configuration.ts
sameerag Jan 20, 2026
4ee54c0
Update names
sameerag Jan 20, 2026
1e24dcb
Update apiExtractor for msal-common
sameerag Jan 20, 2026
827a6ff
Merge branch 'dev' into sso-post-interaction
sameerag Jan 20, 2026
e0f6b03
Merge branch 'dev' into sso-post-interaction
sameerag Feb 8, 2026
cd804d1
Update correlationId - change it to be a different one
sameerag Feb 9, 2026
28cbf36
Address feedback
sameerag Feb 9, 2026
d29ef3d
Merge branch 'dev' into sso-post-interaction
konstantin-msft Feb 10, 2026
6e7f446
Rename appropriately
sameerag Feb 12, 2026
03a70d4
Rename config
sameerag Feb 17, 2026
4e6c400
Merge branch 'dev' into sso-post-interaction
sameerag Feb 17, 2026
6eac154
Update apiExtractor
sameerag Feb 17, 2026
68d2fa1
Update apiextractor in common
sameerag Feb 17, 2026
0cb5c03
maintain uniform casing
sameerag Feb 17, 2026
d0f410c
Update apiextractor
sameerag Feb 18, 2026
a5dce88
Name the parentApi variable appropriately
sameerag Feb 18, 2026
efa1b30
update parentApi in tests
sameerag Feb 18, 2026
e3eb0ab
Update parentApi name
sameerag Feb 18, 2026
6b770c1
Apply suggestions from code review
sameerag Feb 18, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add a background ssoSilent call after interactive calls, with telemetry, [#8252](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8252) ",
"packageName": "@azure/msal-browser",
"email": "sameera.gajjarapu@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add a background ssoSilent call after interactive calls, with telemetry, [#8252](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8252)",
"packageName": "@azure/msal-common",
"email": "sameera.gajjarapu@microsoft.com",
"dependentChangeType": "patch"
}
3 changes: 2 additions & 1 deletion lib/msal-browser/apiReview/msal-browser.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ export type BrowserAuthOptions = {
onRedirectNavigate?: (url: string) => boolean | void;
instanceAware?: boolean;
encodeExtraQueryParams?: boolean;
enableSessionRefresh?: boolean;
};

// Warning: (ae-missing-release-tag) "BrowserCacheLocation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -1830,7 +1831,7 @@ export type WrapperSKU = (typeof WrapperSKU)[keyof typeof WrapperSKU];
// src/cache/LocalStorage.ts:363:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/LocalStorage.ts:426:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/LocalStorage.ts:457:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/config/Configuration.ts:264:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts
// src/config/Configuration.ts:270:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts
// src/event/EventHandler.ts:113:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/event/EventHandler.ts:139:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/index.ts:8:12 - (tsdoc-characters-after-block-tag) The token "@azure" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@"
Expand Down
7 changes: 7 additions & 0 deletions lib/msal-browser/src/config/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export type BrowserAuthOptions = {
* @deprecated This flag is deprecated and will be removed in the next major version where all extra query params will be encoded by default.
*/
encodeExtraQueryParams?: boolean;
/**
* If set to true, MSAL will make a background session refresh call after successful interactive authentication
* (acquireTokenPopup, handleRedirectPromise) to refresh tokens silently.
* This is a boolean flag and defaults to false if not specified.
*/
enableSessionRefresh?: boolean;
};

/** @internal */
Expand Down Expand Up @@ -314,6 +320,7 @@ export function buildConfiguration(
supportsNestedAppAuth: false,
instanceAware: false,
encodeExtraQueryParams: false,
enableSessionRefresh: false,
};

// Default cache options for browser
Expand Down
90 changes: 90 additions & 0 deletions lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,13 @@ export class StandardController implements IController {
undefined,
result.account
);

// Fire-and-forget session refresh in background
this.bkgdSessionRefresh(
result.account,
result.correlationId,
"handleRedirectPromise"
);
} else {
/*
* Instrument an event only if an error code is set. Otherwise, discard it when the redirect response
Expand Down Expand Up @@ -930,6 +937,14 @@ export class StandardController implements IController {
undefined,
result.account
);

// Fire-and-forget session refresh in background
this.bkgdSessionRefresh(
result.account,
result.correlationId,
"acquireTokenPopup"
);

return result;
})
.catch((e: Error) => {
Expand Down Expand Up @@ -984,6 +999,81 @@ export class StandardController implements IController {
visibilityChangeCount: 1,
});
}

/**
* Fire-and-forget session refresh in the background.
* This method makes an iframe request to /authorize to refresh session cookies without calling /token.
* This method does not block the caller and tracks telemetry for success/failure.
* This method only executes if enableSessionRefresh is set to true in the auth configuration.
* @param account - The account to use for the session refresh
* @param correlationId - The correlation ID for telemetry
* @param parentApiId - The API ID of the parent operation for logging purposes
*/
private bkgdSessionRefresh(
account: AccountInfo,
correlationId: string,
parentApiId: string
): void {
// Check if background SSO is enabled
if (!this.config.auth.enableSessionRefresh) {
return;
}

const bgSsoSilentMeasurement = this.performanceClient.startMeasurement(
PerformanceEvents.BackgroundSsoSilent,
correlationId
);
bgSsoSilentMeasurement.add({
parentApiId: parentApiId,
});

this.logger.verbose(
`Background session refresh initiated after ${parentApiId}`,
correlationId
);

/*
* Use setTimeout to ensure this runs in a separate macrotask after the current call stack completes
* This ensures the result is returned to the caller before the session refresh starts and doesn't affect performance
*/
setTimeout(() => {
const ssoSilentRequest: SsoSilentRequest = {
account: account,
correlationId: correlationId,
};

const silentIframeClient =
this.createSilentIframeClient(correlationId);
silentIframeClient
.refreshSession(ssoSilentRequest)
.then((success: boolean) => {
this.logger.verbose(
`Background session refresh completed after ${parentApiId}, success: ${success}`,
correlationId
);
bgSsoSilentMeasurement.end(
{
success: success,
},
undefined,
account
);
})
.catch((error: Error) => {
this.logger.warning(
`Background session refresh failed after ${parentApiId}: ${error.message}`,
correlationId
);
bgSsoSilentMeasurement.end(
{
success: false,
},
error,
account
);
});
}, 0);
}
// #endregion

// #region Silent Flow
Expand Down
136 changes: 136 additions & 0 deletions lib/msal-browser/src/interaction_client/SilentIframeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ProtocolMode,
CommonAuthorizationUrlRequest,
HttpMethod,
AuthorizeProtocol,
} from "@azure/msal-common/browser";
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import { BrowserConfiguration } from "../config/Configuration.js";
Expand Down Expand Up @@ -349,6 +350,141 @@ export class SilentIframeClient extends StandardInteractionClient {
}
}

/**
* Refreshes the session by making an iframe request to /authorize without exchanging the code for tokens.
* This is useful for refreshing session cookies in the background without the overhead of a full token exchange.
* @param request - The SSO silent request
* @returns true if the session was refreshed successfully with a valid authorization code, false otherwise
*/
async refreshSession(request: SsoSilentRequest): Promise<boolean> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.SilentIframeClientAcquireToken,
request.correlationId
);

const inputRequest = { ...request };
if (!inputRequest.prompt) {
inputRequest.prompt = PromptValue.NONE;
}

// Create silent request
const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
request.correlationId
)(inputRequest, InteractionType.Silent);

const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
request.correlationId
)({
serverTelemetryManager: this.initializeServerTelemetryManager(
this.apiId
),
requestAuthority: silentRequest.authority,
requestAzureCloudOptions: silentRequest.azureCloudOptions,
requestExtraQueryParameters: silentRequest.extraQueryParameters,
account: silentRequest.account,
});

const correlationId = silentRequest.correlationId;
const pkceCodes = await invokeAsync(
generatePkceCodes,
PerformanceEvents.GeneratePkceCodes,
this.logger,
this.performanceClient,
correlationId
)(this.performanceClient, this.logger, correlationId);

const requestWithPkce = {
...silentRequest,
codeChallenge: pkceCodes.challenge,
};

// Create authorize request url
const navigateUrl = await invokeAsync(
Authorize.getAuthCodeRequestUrl,
PerformanceEvents.GetAuthCodeUrl,
this.logger,
this.performanceClient,
correlationId
)(
this.config,
authClient.authority,
requestWithPkce,
this.logger,
this.performanceClient
);

// Get the frame handle for the silent request - this triggers the session refresh
const msalFrame = await invokeAsync(
initiateCodeRequest,
PerformanceEvents.SilentHandlerInitiateAuthRequest,
this.logger,
this.performanceClient,
correlationId
)(
navigateUrl,
this.performanceClient,
this.logger,
correlationId,
this.config.system.navigateFrameWait
);

const responseType = this.config.auth.OIDCOptions.serverResponseType;
// Monitor the iframe for the response
const responseString = await invokeAsync(
monitorIframeForHash,
PerformanceEvents.SilentHandlerMonitorIframeForHash,
this.logger,
this.performanceClient,
correlationId
)(
msalFrame,
this.config.system.iframeHashTimeout,
this.config.system.pollIntervalMilliseconds,
this.performanceClient,
this.logger,
correlationId,
responseType
);

// Deserialize the response
const serverParams = invoke(
ResponseHandler.deserializeResponse,
PerformanceEvents.DeserializeResponse,
this.logger,
this.performanceClient,
correlationId
)(responseString, responseType, this.logger);

// Validate the response - this checks for errors and validates state
AuthorizeProtocol.validateAuthorizationResponse(
serverParams,
silentRequest.state
);

// Verify a valid authorization code is present
if (!serverParams.code) {
this.logger.warning(
"Session refresh response did not contain an authorization code",
correlationId
);
return false;
}

this.logger.verbose(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we factor out the shared code with ssoSilent? Any distinct PerformanceEvent names/log messages can be parameterized. I'd rather we not have 2 sources of truth for the iframe flow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No shared code w.r.t events. Can you clarify what exactly you mean?

"Session refresh completed successfully with valid authorization code - skipped token exchange",
correlationId
);
return true;
}

/**
* Currently Unsupported
*/
Expand Down
Loading
Loading