Skip to content

Commit de0cf51

Browse files
sameeragCopilotCopilotkonstantin-msft
authored
Verify SSO Capability (#8252)
This PR verfies if an app is capable of SSO after a successful interactive authentication. When enabled via the new `verifySso` configuration option, MSAL will fire a fire-and-forget `verifySso()` call after handleRedirectPromise and acquireTokenPopup complete successfully. Changes: - Added new `verifySso` configuration option (defaults to false) - Implemented fire-and-forget background verifySso after interactive authentication - Added comprehensive telemetry tracking with new `SsoCapable` performance event --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Konstantin <kshabelko@microsoft.com>
1 parent 6b01484 commit de0cf51

9 files changed

Lines changed: 995 additions & 54 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Verify if an app is SSO Capable, [#8252](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8252) ",
4+
"packageName": "@azure/msal-browser",
5+
"email": "sameera.gajjarapu@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Verify if an app is SSO Capable, [#8252](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8252)",
4+
"packageName": "@azure/msal-common",
5+
"email": "sameera.gajjarapu@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/apiReview/msal-browser.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ export type BrowserAuthOptions = {
463463
onRedirectNavigate?: (url: string) => boolean | void;
464464
instanceAware?: boolean;
465465
encodeExtraQueryParams?: boolean;
466+
verifySSO?: boolean;
466467
};
467468

468469
// 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)
@@ -1830,7 +1831,7 @@ export type WrapperSKU = (typeof WrapperSKU)[keyof typeof WrapperSKU];
18301831
// 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
18311832
// 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
18321833
// 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
1833-
// src/config/Configuration.ts:264:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts
1834+
// src/config/Configuration.ts:271:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts
18341835
// 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
18351836
// 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
18361837
// 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 "@"

lib/msal-browser/src/config/Configuration.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ export type BrowserAuthOptions = {
113113
* @deprecated This flag is deprecated and will be removed in the next major version where all extra query params will be encoded by default.
114114
*/
115115
encodeExtraQueryParams?: boolean;
116+
/**
117+
* If set to true, MSAL will make a background SSO verification call after successful interactive authentication.
118+
* COGS intensive, recommendation is to *NOT* set this flag to true unless your application has a specific need for it.
119+
* This will trigger additional network calls after interactive authentication flows (acquireTokenPopup, handleRedirectPromise) calls.
120+
* This is a boolean flag and defaults to false if not specified.
121+
*/
122+
verifySSO?: boolean;
116123
};
117124

118125
/** @internal */
@@ -314,6 +321,7 @@ export function buildConfiguration(
314321
supportsNestedAppAuth: false,
315322
instanceAware: false,
316323
encodeExtraQueryParams: false,
324+
verifySSO: false,
317325
};
318326

319327
// Default cache options for browser

lib/msal-browser/src/controllers/StandardController.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ export class StandardController implements IController {
567567
undefined,
568568
result.account
569569
);
570+
571+
// Fire-and-forget SSO capability verification in background
572+
this.verifySsoCapability(
573+
result.account,
574+
InteractionType.Redirect
575+
);
570576
} else {
571577
/*
572578
* Instrument an event only if an error code is set. Otherwise, discard it when the redirect response
@@ -930,6 +936,10 @@ export class StandardController implements IController {
930936
undefined,
931937
result.account
932938
);
939+
940+
// SSO capability verification in background
941+
this.verifySsoCapability(result.account, InteractionType.Popup);
942+
933943
return result;
934944
})
935945
.catch((e: Error) => {
@@ -984,6 +994,79 @@ export class StandardController implements IController {
984994
visibilityChangeCount: 1,
985995
});
986996
}
997+
998+
/**
999+
* SSO capability verification in the background.
1000+
* This method makes an iframe request to /authorize to verify SSO capability without calling /token.
1001+
* This method does not block the caller and tracks telemetry for success/failure.
1002+
* This method only executes if verifySSO is set to true in the auth configuration.
1003+
* @param account - The account to use for the SSO verification
1004+
* @param parentApi - The API ID of the parent operation for logging purposes
1005+
*/
1006+
private verifySsoCapability(account: AccountInfo, parentApi: string): void {
1007+
// Check if SSO capability verification is enabled
1008+
if (!this.config.auth.verifySSO) {
1009+
return;
1010+
}
1011+
1012+
const correlationId = this.browserCrypto.createNewGuid();
1013+
const ssoCapableMeasurement = this.performanceClient.startMeasurement(
1014+
PerformanceEvents.SsoCapable,
1015+
correlationId
1016+
);
1017+
ssoCapableMeasurement.add({
1018+
parentApi: parentApi,
1019+
});
1020+
1021+
this.logger.verbose(
1022+
`SSO capability verification initiated after ${parentApi}`,
1023+
correlationId
1024+
);
1025+
1026+
/*
1027+
* Use setTimeout to ensure this runs in a separate macrotask after the current call stack completes
1028+
* This ensures the result is returned to the caller before the SSO verification starts and doesn't affect performance
1029+
*/
1030+
setTimeout(() => {
1031+
const ssoVerificationRequest: SsoSilentRequest = {
1032+
account: account,
1033+
correlationId: correlationId,
1034+
};
1035+
1036+
const silentIframeClient =
1037+
this.createSilentIframeClient(correlationId);
1038+
silentIframeClient
1039+
.verifySso(ssoVerificationRequest)
1040+
.then((success: boolean) => {
1041+
this.logger.verbose(
1042+
`SSO capability verification completed after ${parentApi}, success: ${success}`,
1043+
correlationId
1044+
);
1045+
ssoCapableMeasurement.end(
1046+
{
1047+
fromCache: false,
1048+
success: success,
1049+
},
1050+
undefined,
1051+
account
1052+
);
1053+
})
1054+
.catch((error: Error) => {
1055+
this.logger.warning(
1056+
`SSO capability verification failed after ${parentApi}: ${error.message}`,
1057+
correlationId
1058+
);
1059+
ssoCapableMeasurement.end(
1060+
{
1061+
fromCache: false,
1062+
success: false,
1063+
},
1064+
error,
1065+
account
1066+
);
1067+
});
1068+
}, 0);
1069+
}
9871070
// #endregion
9881071

9891072
// #region Silent Flow

lib/msal-browser/src/interaction_client/SilentIframeClient.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ProtocolMode,
1717
CommonAuthorizationUrlRequest,
1818
HttpMethod,
19+
AuthorizeProtocol,
1920
} from "@azure/msal-common/browser";
2021
import { StandardInteractionClient } from "./StandardInteractionClient.js";
2122
import { BrowserConfiguration } from "../config/Configuration.js";
@@ -349,6 +350,141 @@ export class SilentIframeClient extends StandardInteractionClient {
349350
}
350351
}
351352

353+
/**
354+
* Verifies SSO capability by making an iframe request to /authorize without exchanging the code for tokens.
355+
* This is useful for verifying SSO capability in the background without the overhead of a full token exchange.
356+
* @param request - The SSO silent request
357+
* @returns true if SSO verification was successful with a valid authorization code, false otherwise
358+
*/
359+
async verifySso(request: SsoSilentRequest): Promise<boolean> {
360+
this.performanceClient.addQueueMeasurement(
361+
PerformanceEvents.SilentIframeClientAcquireToken,
362+
request.correlationId
363+
);
364+
365+
const inputRequest = { ...request };
366+
if (!inputRequest.prompt) {
367+
inputRequest.prompt = PromptValue.NONE;
368+
}
369+
370+
// Create silent request
371+
const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync(
372+
this.initializeAuthorizationRequest.bind(this),
373+
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
374+
this.logger,
375+
this.performanceClient,
376+
request.correlationId
377+
)(inputRequest, InteractionType.Silent);
378+
379+
const authClient = await invokeAsync(
380+
this.createAuthCodeClient.bind(this),
381+
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
382+
this.logger,
383+
this.performanceClient,
384+
request.correlationId
385+
)({
386+
serverTelemetryManager: this.initializeServerTelemetryManager(
387+
this.apiId
388+
),
389+
requestAuthority: silentRequest.authority,
390+
requestAzureCloudOptions: silentRequest.azureCloudOptions,
391+
requestExtraQueryParameters: silentRequest.extraQueryParameters,
392+
account: silentRequest.account,
393+
});
394+
395+
const correlationId = silentRequest.correlationId;
396+
const pkceCodes = await invokeAsync(
397+
generatePkceCodes,
398+
PerformanceEvents.GeneratePkceCodes,
399+
this.logger,
400+
this.performanceClient,
401+
correlationId
402+
)(this.performanceClient, this.logger, correlationId);
403+
404+
const requestWithPkce = {
405+
...silentRequest,
406+
codeChallenge: pkceCodes.challenge,
407+
};
408+
409+
// Create authorize request url
410+
const navigateUrl = await invokeAsync(
411+
Authorize.getAuthCodeRequestUrl,
412+
PerformanceEvents.GetAuthCodeUrl,
413+
this.logger,
414+
this.performanceClient,
415+
correlationId
416+
)(
417+
this.config,
418+
authClient.authority,
419+
requestWithPkce,
420+
this.logger,
421+
this.performanceClient
422+
);
423+
424+
// Get the frame handle for the silent request - this triggers the SSO verification
425+
const msalFrame = await invokeAsync(
426+
initiateCodeRequest,
427+
PerformanceEvents.SilentHandlerInitiateAuthRequest,
428+
this.logger,
429+
this.performanceClient,
430+
correlationId
431+
)(
432+
navigateUrl,
433+
this.performanceClient,
434+
this.logger,
435+
correlationId,
436+
this.config.system.navigateFrameWait
437+
);
438+
439+
const responseType = this.config.auth.OIDCOptions.serverResponseType;
440+
// Monitor the iframe for the response
441+
const responseString = await invokeAsync(
442+
monitorIframeForHash,
443+
PerformanceEvents.SilentHandlerMonitorIframeForHash,
444+
this.logger,
445+
this.performanceClient,
446+
correlationId
447+
)(
448+
msalFrame,
449+
this.config.system.iframeHashTimeout,
450+
this.config.system.pollIntervalMilliseconds,
451+
this.performanceClient,
452+
this.logger,
453+
correlationId,
454+
responseType
455+
);
456+
457+
// Deserialize the response
458+
const serverParams = invoke(
459+
ResponseHandler.deserializeResponse,
460+
PerformanceEvents.DeserializeResponse,
461+
this.logger,
462+
this.performanceClient,
463+
correlationId
464+
)(responseString, responseType, this.logger);
465+
466+
// Validate the response - this checks for errors and validates state
467+
AuthorizeProtocol.validateAuthorizationResponse(
468+
serverParams,
469+
silentRequest.state
470+
);
471+
472+
// Verify a valid authorization code is present
473+
if (!serverParams.code) {
474+
this.logger.warning(
475+
"SSO verification response did not contain an authorization code",
476+
correlationId
477+
);
478+
return false;
479+
}
480+
481+
this.logger.verbose(
482+
"SSO verification completed successfully with valid authorization code - skipped token exchange",
483+
correlationId
484+
);
485+
return true;
486+
}
487+
352488
/**
353489
* Currently Unsupported
354490
*/

0 commit comments

Comments
 (0)