/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
|
import {
|
ICrypto,
|
Logger,
|
Constants,
|
AuthorizationCodeClient,
|
AuthError,
|
IPerformanceClient,
|
PerformanceEvents,
|
invokeAsync,
|
invoke,
|
ProtocolMode,
|
CommonAuthorizationUrlRequest,
|
AuthorizeProtocol,
|
PkceCodes,
|
} from "@azure/msal-common/browser";
|
import {
|
initializeAuthorizationRequest,
|
StandardInteractionClient,
|
} from "./StandardInteractionClient.js";
|
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
|
import { BrowserConfiguration } from "../config/Configuration.js";
|
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
|
import { EventHandler } from "../event/EventHandler.js";
|
import { INavigationClient } from "../navigation/INavigationClient.js";
|
import {
|
createBrowserAuthError,
|
BrowserAuthErrorCodes,
|
} from "../error/BrowserAuthError.js";
|
import {
|
InteractionType,
|
ApiId,
|
BrowserConstants,
|
} from "../utils/BrowserConstants.js";
|
import {
|
initiateCodeRequest,
|
initiateCodeFlowWithPost,
|
initiateEarRequest,
|
removeHiddenIframe,
|
} from "../interaction_handler/SilentHandler.js";
|
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
|
import { AuthenticationResult } from "../response/AuthenticationResult.js";
|
import * as BrowserUtils from "../utils/BrowserUtils.js";
|
import * as ResponseHandler from "../response/ResponseHandler.js";
|
import * as Authorize from "../protocol/Authorize.js";
|
import { generatePkceCodes } from "../crypto/PkceGenerator.js";
|
import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js";
|
import { generateEarKey } from "../crypto/BrowserCrypto.js";
|
import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js";
|
import {
|
getDiscoveredAuthority,
|
initializeServerTelemetryManager,
|
} from "./BaseInteractionClient.js";
|
|
export class SilentIframeClient extends StandardInteractionClient {
|
protected apiId: ApiId;
|
protected nativeStorage: BrowserCacheManager;
|
|
constructor(
|
config: BrowserConfiguration,
|
storageImpl: BrowserCacheManager,
|
browserCrypto: ICrypto,
|
logger: Logger,
|
eventHandler: EventHandler,
|
navigationClient: INavigationClient,
|
apiId: ApiId,
|
performanceClient: IPerformanceClient,
|
nativeStorageImpl: BrowserCacheManager,
|
correlationId: string,
|
platformAuthProvider?: IPlatformAuthHandler
|
) {
|
super(
|
config,
|
storageImpl,
|
browserCrypto,
|
logger,
|
eventHandler,
|
navigationClient,
|
performanceClient,
|
correlationId,
|
platformAuthProvider
|
);
|
this.apiId = apiId;
|
this.nativeStorage = nativeStorageImpl;
|
}
|
|
/**
|
* Acquires a token silently by opening a hidden iframe to the /authorize endpoint with prompt=none or prompt=no_session
|
* @param request
|
*/
|
async acquireToken(
|
request: SsoSilentRequest
|
): Promise<AuthenticationResult> {
|
// Check that we have some SSO data
|
if (
|
!request.loginHint &&
|
!request.sid &&
|
(!request.account || !request.account.username)
|
) {
|
this.logger.warning(
|
"No user hint provided. The authorization server may need more information to complete this request.",
|
this.correlationId
|
);
|
}
|
|
// Check the prompt value
|
const inputRequest = { ...request };
|
if (inputRequest.prompt) {
|
if (
|
inputRequest.prompt !== Constants.PromptValue.NONE &&
|
inputRequest.prompt !== Constants.PromptValue.NO_SESSION
|
) {
|
this.logger.warning(
|
`SilentIframeClient. Replacing invalid prompt '${inputRequest.prompt}' with '${Constants.PromptValue.NONE}'`,
|
this.correlationId
|
);
|
inputRequest.prompt = Constants.PromptValue.NONE;
|
}
|
} else {
|
inputRequest.prompt = Constants.PromptValue.NONE;
|
}
|
|
// Create silent request
|
const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync(
|
initializeAuthorizationRequest,
|
BrowserPerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
inputRequest,
|
InteractionType.Silent,
|
this.config,
|
this.browserCrypto,
|
this.browserStorage,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
);
|
silentRequest.platformBroker = isPlatformAuthAllowed(
|
this.config,
|
this.logger,
|
this.correlationId,
|
this.platformAuthProvider,
|
silentRequest.authenticationScheme
|
);
|
BrowserUtils.preconnect(silentRequest.authority);
|
|
if (this.config.system.protocolMode === ProtocolMode.EAR) {
|
return this.executeEarFlow(silentRequest);
|
} else {
|
return this.executeCodeFlow(silentRequest);
|
}
|
}
|
|
/**
|
* Executes auth code + PKCE flow
|
* @param request
|
* @returns
|
*/
|
async executeCodeFlow(
|
request: CommonAuthorizationUrlRequest
|
): Promise<AuthenticationResult> {
|
let authClient: AuthorizationCodeClient | undefined;
|
const serverTelemetryManager = initializeServerTelemetryManager(
|
this.apiId,
|
this.config.auth.clientId,
|
this.correlationId,
|
this.browserStorage,
|
this.logger
|
);
|
|
try {
|
// Initialize the client
|
authClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)({
|
serverTelemetryManager,
|
requestAuthority: request.authority,
|
requestAzureCloudOptions: request.azureCloudOptions,
|
requestExtraQueryParameters: request.extraQueryParameters,
|
account: request.account,
|
});
|
|
return await invokeAsync(
|
this.silentTokenHelper.bind(this),
|
BrowserPerformanceEvents.SilentIframeClientTokenHelper,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(authClient, request);
|
} catch (e) {
|
if (e instanceof AuthError) {
|
(e as AuthError).setCorrelationId(this.correlationId);
|
serverTelemetryManager.cacheFailedRequest(e);
|
}
|
|
if (
|
!authClient ||
|
!(e instanceof AuthError) ||
|
e.errorCode !== BrowserConstants.INVALID_GRANT_ERROR
|
) {
|
throw e;
|
}
|
|
this.performanceClient.addFields(
|
{
|
retryError: e.errorCode,
|
},
|
this.correlationId
|
);
|
|
return await invokeAsync(
|
this.silentTokenHelper.bind(this),
|
BrowserPerformanceEvents.SilentIframeClientTokenHelper,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(authClient, request);
|
}
|
}
|
|
/**
|
* Executes EAR flow
|
* @param request
|
*/
|
async executeEarFlow(
|
request: CommonAuthorizationUrlRequest
|
): Promise<AuthenticationResult> {
|
const {
|
correlationId,
|
authority,
|
azureCloudOptions,
|
extraQueryParameters,
|
account,
|
} = request;
|
const discoveredAuthority = await invokeAsync(
|
getDiscoveredAuthority,
|
BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
this.correlationId,
|
this.performanceClient,
|
this.browserStorage,
|
this.logger,
|
authority,
|
azureCloudOptions,
|
extraQueryParameters,
|
account
|
);
|
|
const earJwk = await invokeAsync(
|
generateEarKey,
|
BrowserPerformanceEvents.GenerateEarKey,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)();
|
const pkceCodes = await invokeAsync(
|
generatePkceCodes,
|
BrowserPerformanceEvents.GeneratePkceCodes,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(this.performanceClient, this.logger, correlationId);
|
const silentRequest = {
|
...request,
|
earJwk: earJwk,
|
codeChallenge: pkceCodes.challenge,
|
};
|
const iframe = await invokeAsync(
|
initiateEarRequest,
|
BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
discoveredAuthority,
|
silentRequest,
|
this.logger,
|
this.performanceClient
|
);
|
|
const responseType = this.config.auth.OIDCOptions.responseMode;
|
let responseString: string;
|
try {
|
responseString = await invokeAsync(
|
BrowserUtils.waitForBridgeResponse,
|
BrowserPerformanceEvents.SilentHandlerMonitorIframeForHash,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config.system.iframeBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
request,
|
this.performanceClient,
|
this.config.experimental
|
);
|
} finally {
|
invoke(
|
removeHiddenIframe,
|
BrowserPerformanceEvents.RemoveHiddenIframe,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(iframe);
|
}
|
|
const serverParams = invoke(
|
ResponseHandler.deserializeResponse,
|
BrowserPerformanceEvents.DeserializeResponse,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(responseString, responseType, this.logger, this.correlationId);
|
|
if (!serverParams.ear_jwe && serverParams.code) {
|
// If server doesn't support EAR, they may fallback to auth code flow instead
|
const authClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)({
|
serverTelemetryManager: initializeServerTelemetryManager(
|
this.apiId,
|
this.config.auth.clientId,
|
correlationId,
|
this.browserStorage,
|
this.logger
|
),
|
requestAuthority: request.authority,
|
requestAzureCloudOptions: request.azureCloudOptions,
|
requestExtraQueryParameters: request.extraQueryParameters,
|
account: request.account,
|
authority: discoveredAuthority,
|
});
|
|
return invokeAsync(
|
Authorize.handleResponseCode,
|
BrowserPerformanceEvents.HandleResponseCode,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
silentRequest,
|
serverParams,
|
pkceCodes.verifier,
|
this.apiId,
|
this.config,
|
authClient,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
} else {
|
return invokeAsync(
|
Authorize.handleResponseEAR,
|
BrowserPerformanceEvents.HandleResponseEar,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
silentRequest,
|
serverParams,
|
this.apiId,
|
this.config,
|
discoveredAuthority,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
}
|
}
|
|
/**
|
* Verifies SSO capability by making an iframe request to /authorize without exchanging the code for tokens.
|
* This is useful for verifying SSO capability in the background without the overhead of a full token exchange.
|
* @param request - The SSO silent request
|
* @returns true if SSO verification was successful with a valid authorization code, false otherwise
|
*/
|
async verifySso(request: SsoSilentRequest): Promise<boolean> {
|
const inputRequest = { ...request };
|
if (!inputRequest.prompt) {
|
inputRequest.prompt = Constants.PromptValue.NONE;
|
}
|
|
// Create silent request
|
const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync(
|
initializeAuthorizationRequest,
|
BrowserPerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
inputRequest,
|
InteractionType.Silent,
|
this.config,
|
this.browserCrypto,
|
this.browserStorage,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
);
|
|
const authClient: AuthorizationCodeClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)({
|
serverTelemetryManager: initializeServerTelemetryManager(
|
this.apiId,
|
this.config.auth.clientId,
|
this.correlationId,
|
this.browserStorage,
|
this.logger
|
),
|
requestAuthority: silentRequest.authority,
|
requestAzureCloudOptions: silentRequest.azureCloudOptions,
|
requestExtraQueryParameters: silentRequest.extraQueryParameters,
|
account: silentRequest.account,
|
});
|
|
const { serverParams } = await this.silentAuthorizeHelper(
|
authClient,
|
silentRequest
|
);
|
|
const correlationId = silentRequest.correlationId;
|
|
// 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(
|
"SSO verification response did not contain an authorization code",
|
correlationId
|
);
|
return false;
|
}
|
|
this.logger.verbose(
|
"SSO verification completed successfully with valid authorization code - skipped token exchange",
|
correlationId
|
);
|
return true;
|
}
|
|
/**
|
* Currently Unsupported
|
*/
|
logout(): Promise<void> {
|
// Synchronous so we must reject
|
return Promise.reject(
|
createBrowserAuthError(
|
BrowserAuthErrorCodes.silentLogoutUnsupported
|
)
|
);
|
}
|
|
/**
|
* Helper which acquires an authorization code silently using a hidden iframe from given url
|
* using the scopes requested as part of the id, and exchanges the code for a set of OAuth tokens.
|
* @param navigateUrl
|
* @param userRequestScopes
|
*/
|
protected async silentTokenHelper(
|
authClient: AuthorizationCodeClient,
|
request: CommonAuthorizationUrlRequest
|
): Promise<AuthenticationResult> {
|
const { serverParams, pkceCodes } = await this.silentAuthorizeHelper(
|
authClient,
|
request
|
);
|
|
return invokeAsync(
|
Authorize.handleResponseCode,
|
BrowserPerformanceEvents.HandleResponseCode,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(
|
request,
|
serverParams,
|
pkceCodes.verifier,
|
this.apiId,
|
this.config,
|
authClient,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
}
|
|
/**
|
* Shared helper that generates PKCE codes, builds the /authorize URL,
|
* loads it in a hidden iframe, waits for the redirect-bridge response,
|
* and returns the deserialized server parameters along with the PKCE codes
|
* and the request that was sent.
|
*/
|
private async silentAuthorizeHelper(
|
authClient: AuthorizationCodeClient,
|
request: CommonAuthorizationUrlRequest
|
): Promise<{
|
serverParams: ReturnType<typeof ResponseHandler.deserializeResponse>;
|
pkceCodes: PkceCodes;
|
silentRequest: CommonAuthorizationUrlRequest;
|
}> {
|
const correlationId = request.correlationId;
|
const pkceCodes = await invokeAsync(
|
generatePkceCodes,
|
BrowserPerformanceEvents.GeneratePkceCodes,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(this.performanceClient, this.logger, correlationId);
|
|
const silentRequest = {
|
...request,
|
codeChallenge: pkceCodes.challenge,
|
};
|
|
let iframe: HTMLIFrameElement;
|
if (request.httpMethod === Constants.HttpMethod.POST) {
|
iframe = await invokeAsync(
|
initiateCodeFlowWithPost,
|
BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
authClient.authority,
|
silentRequest,
|
this.logger,
|
this.performanceClient
|
);
|
} else {
|
// Create authorize request url
|
const navigateUrl = await invokeAsync(
|
Authorize.getAuthCodeRequestUrl,
|
PerformanceEvents.GetAuthCodeUrl,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
authClient.authority,
|
silentRequest,
|
this.logger,
|
this.performanceClient
|
);
|
|
// Get the frame handle for the silent request
|
iframe = await invokeAsync(
|
initiateCodeRequest,
|
BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(navigateUrl, this.performanceClient, this.logger, correlationId);
|
}
|
|
const responseType = this.config.auth.OIDCOptions.responseMode;
|
// Wait for response from the redirect bridge.
|
let responseString: string;
|
try {
|
responseString = await invokeAsync(
|
BrowserUtils.waitForBridgeResponse,
|
BrowserPerformanceEvents.SilentHandlerMonitorIframeForHash,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config.system.iframeBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
request,
|
this.performanceClient,
|
this.config.experimental
|
);
|
} finally {
|
invoke(
|
removeHiddenIframe,
|
BrowserPerformanceEvents.RemoveHiddenIframe,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(iframe);
|
}
|
|
const serverParams = invoke(
|
ResponseHandler.deserializeResponse,
|
BrowserPerformanceEvents.DeserializeResponse,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(responseString, responseType, this.logger, this.correlationId);
|
|
return { serverParams, pkceCodes, silentRequest };
|
}
|
}
|