Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+832
View File
@@ -0,0 +1,832 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseClient } from "./BaseClient.js";
import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js";
import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js";
import { Authority } from "../authority/Authority.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import {
GrantType,
AuthenticationScheme,
PromptValue,
Separators,
HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import {
ClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { StringUtils } from "../utils/StringUtils.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { UrlString } from "../url/UrlString.js";
import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js";
import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { AccountInfo } from "../account/AccountInfo.js";
import {
buildClientInfoFromHomeAccountId,
buildClientInfo,
} from "../account/ClientInfo.js";
import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { RequestValidator } from "../request/RequestValidator.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
/**
* Oauth2.0 Authorization Code client
* @internal
*/
export class AuthorizationCodeClient extends BaseClient {
// Flag to indicate if client is for hybrid spa auth code redemption
protected includeRedirectUri: boolean = true;
private oidcDefaultScopes;
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
this.oidcDefaultScopes =
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes;
}
/**
* Creates the URL of the authorization request letting the user input credentials and consent to the
* application. The URL target the /authorize endpoint of the authority configured in the
* application object.
*
* Once the user inputs their credentials and consents, the authority will send a response to the redirect URI
* sent in the request and should contain an authorization code, which can then be used to acquire tokens via
* acquireToken(AuthorizationCodeRequest)
* @param request
*/
async getAuthCodeUrl(
request: CommonAuthorizationUrlRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.GetAuthCodeUrl,
request.correlationId
);
const queryString = await invokeAsync(
this.createAuthCodeUrlQueryString.bind(this),
PerformanceEvents.AuthClientCreateQueryString,
this.logger,
this.performanceClient,
request.correlationId
)(request);
return UrlString.appendQueryString(
this.authority.authorizationEndpoint,
queryString
);
}
/**
* API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the
* authorization_code_grant
* @param request
*/
async acquireToken(
request: CommonAuthorizationCodeRequest,
authCodePayload?: AuthorizationCodePayload
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientAcquireToken,
request.correlationId
);
if (!request.code) {
throw createClientAuthError(
ClientAuthErrorCodes.requestCannotBeMade
);
}
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.AuthClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(this.authority, request);
// Retrieve requestId from response headers
const requestId = response.headers?.[HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin,
this.performanceClient
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(response.body);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
authCodePayload,
undefined,
undefined,
undefined,
requestId
);
}
/**
* Handles the hash fragment response from public client code request. Returns a code response used by
* the client to exchange for a token in acquireToken.
* @param hashFragment
*/
handleFragmentResponse(
serverParams: ServerAuthorizationCodeResponse,
cachedState: string
): AuthorizationCodePayload {
// Handle responses.
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
null,
null
);
// Get code response
responseHandler.validateServerAuthorizationCodeResponse(
serverParams,
cachedState
);
// throw when there is no auth code in the response
if (!serverParams.code) {
throw createClientAuthError(
ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse
);
}
return serverParams as AuthorizationCodePayload;
}
/**
* Used to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param authorityUri
*/
getLogoutUri(logoutRequest: CommonEndSessionRequest): string {
// Throw error if logoutRequest is null/undefined
if (!logoutRequest) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.logoutRequestEmpty
);
}
const queryString = this.createLogoutUrlQueryString(logoutRequest);
// Construct logout URI
return UrlString.appendQueryString(
this.authority.endSessionEndpoint,
queryString
);
}
/**
* Executes POST request to token endpoint
* @param authority
* @param request
*/
private async executeTokenRequest(
authority: Authority,
request: CommonAuthorizationCodeRequest
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientExecuteTokenRequest,
request.correlationId
);
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await invokeAsync(
this.createTokenRequestBody.bind(this),
PerformanceEvents.AuthClientCreateTokenRequestBody,
this.logger,
this.performanceClient,
request.correlationId
)(request);
let ccsCredential: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCredential = {
credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
"Could not parse client info for CCS Header: " + e
);
}
}
const headers: Record<string, string> = this.createTokenRequestHeaders(
ccsCredential || request.ccsCredential
);
const thumbprint: RequestThumbprint = {
clientId:
request.tokenBodyParameters?.clientId ||
this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return invokeAsync(
this.executePostToTokenEndpoint.bind(this),
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint
);
}
/**
* Generates a map for all the params to be sent to the service
* @param request
*/
private async createTokenRequestBody(
request: CommonAuthorizationCodeRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientCreateTokenRequestBody,
request.correlationId
);
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
/*
* For hybrid spa flow, there will be a code but no verifier
* In this scenario, don't include redirect uri as auth code will not be bound to redirect URI
*/
if (!this.includeRedirectUri) {
// Just validate
RequestValidator.validateRedirectUri(request.redirectUri);
} else {
// Validate and include redirect uri
parameterBuilder.addRedirectUri(request.redirectUri);
}
// Add scope array, parameter builder will add default scopes and dedupe
parameterBuilder.addScopes(
request.scopes,
true,
this.oidcDefaultScopes
);
// add code: user set, not validated
parameterBuilder.addAuthorizationCode(request.code);
// Add library metadata
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
// add code_verifier if passed
if (request.codeVerifier) {
parameterBuilder.addCodeVerifier(request.codeVerifier);
}
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT);
parameterBuilder.addClientInfo();
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils,
this.performanceClient
);
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
// SPA PoP requires full Base64Url encoded req_cnf string (unhashed)
parameterBuilder.addPopToken(reqCnfData);
} else if (request.authenticationScheme === AuthenticationScheme.SSH) {
if (request.sshJwk) {
parameterBuilder.addSshJwk(request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
let ccsCred: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCred = {
credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
"Could not parse client info for CCS Header: " + e
);
}
} else {
ccsCred = request.ccsCredential;
}
// Adds these as parameters in the request instead of headers to prevent CORS preflight request
if (this.config.systemOptions.preventCorsPreflight && ccsCred) {
switch (ccsCred.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
ccsCred.credential
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
parameterBuilder.addCcsUpn(ccsCred.credential);
break;
}
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenBodyParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenBodyParameters
);
}
// Add hybrid spa parameters if not already provided
if (
request.enableSpaAuthorizationCode &&
(!request.tokenBodyParameters ||
!request.tokenBodyParameters[
AADServerParamKeys.RETURN_SPA_CODE
])
) {
parameterBuilder.addExtraQueryParameters({
[AADServerParamKeys.RETURN_SPA_CODE]: "1",
});
}
return parameterBuilder.createQueryString();
}
/**
* This API validates the `AuthorizationCodeUrlRequest` and creates a URL
* @param request
*/
private async createAuthCodeUrlQueryString(
request: CommonAuthorizationUrlRequest
): Promise<string> {
// generate the correlationId if not set by the user and add
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientCreateQueryString,
correlationId
);
const parameterBuilder = new RequestParameterBuilder(
correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
const requestScopes = [
...(request.scopes || []),
...(request.extraScopesToConsent || []),
];
parameterBuilder.addScopes(requestScopes, true, this.oidcDefaultScopes);
// validate the redirectUri (to be a non null value)
parameterBuilder.addRedirectUri(request.redirectUri);
parameterBuilder.addCorrelationId(correlationId);
// add response_mode. If not passed in it defaults to query.
parameterBuilder.addResponseMode(request.responseMode);
// add response_type = code
parameterBuilder.addResponseTypeCode();
// add library info parameters
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
if (!isOidcProtocolMode(this.config)) {
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
}
// add client_info=1
parameterBuilder.addClientInfo();
if (request.codeChallenge && request.codeChallengeMethod) {
parameterBuilder.addCodeChallengeParams(
request.codeChallenge,
request.codeChallengeMethod
);
}
if (request.prompt) {
parameterBuilder.addPrompt(request.prompt);
}
if (request.domainHint) {
parameterBuilder.addDomainHint(request.domainHint);
this.performanceClient?.addFields(
{ domainHintFromRequest: true },
correlationId
);
}
this.performanceClient?.addFields(
{ prompt: request.prompt },
correlationId
);
// Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object
if (request.prompt !== PromptValue.SELECT_ACCOUNT) {
// AAD will throw if prompt=select_account is passed with an account hint
if (request.sid && request.prompt === PromptValue.NONE) {
// SessionID is only used in silent calls
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from request"
);
parameterBuilder.addSid(request.sid);
this.performanceClient?.addFields(
{ sidFromRequest: true },
correlationId
);
} else if (request.account) {
const accountSid = this.extractAccountSid(request.account);
let accountLoginHintClaim = this.extractLoginHint(
request.account
);
if (accountLoginHintClaim && request.domainHint) {
this.logger.warning(
`AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint`
);
accountLoginHintClaim = null;
}
// If login_hint claim is present, use it over sid/username
if (accountLoginHintClaim) {
this.logger.verbose(
"createAuthCodeUrlQueryString: login_hint claim present on account"
);
parameterBuilder.addLoginHint(accountLoginHintClaim);
this.performanceClient?.addFields(
{ loginHintFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
} else if (accountSid && request.prompt === PromptValue.NONE) {
/*
* If account and loginHint are provided, we will check account first for sid before adding loginHint
* SessionId is only used in silent calls
*/
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from account"
);
parameterBuilder.addSid(accountSid);
this.performanceClient?.addFields(
{ sidFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
} else if (request.loginHint) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from request"
);
parameterBuilder.addLoginHint(request.loginHint);
parameterBuilder.addCcsUpn(request.loginHint);
this.performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
} else if (request.account.username) {
// Fallback to account username if provided
this.logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from account"
);
parameterBuilder.addLoginHint(request.account.username);
this.performanceClient?.addFields(
{ loginHintFromUpn: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
}
} else if (request.loginHint) {
this.logger.verbose(
"createAuthCodeUrlQueryString: No account, adding login_hint from request"
);
parameterBuilder.addLoginHint(request.loginHint);
parameterBuilder.addCcsUpn(request.loginHint);
this.performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
}
} else {
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints"
);
}
if (request.nonce) {
parameterBuilder.addNonce(request.nonce);
}
if (request.state) {
parameterBuilder.addState(request.state);
}
if (
request.claims ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
this.addExtraQueryParams(request, parameterBuilder);
if (request.platformBroker) {
// signal ests that this is a WAM call
parameterBuilder.addNativeBroker();
// pass the req_cnf for POP
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils
);
// req_cnf is always sent as a string for SPAs
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
parameterBuilder.addPopToken(reqCnfData);
}
}
return parameterBuilder.createQueryString();
}
/**
* This API validates the `EndSessionRequest` and creates a URL
* @param request
*/
private createLogoutUrlQueryString(
request: CommonEndSessionRequest
): string {
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
if (request.postLogoutRedirectUri) {
parameterBuilder.addPostLogoutRedirectUri(
request.postLogoutRedirectUri
);
}
if (request.correlationId) {
parameterBuilder.addCorrelationId(request.correlationId);
}
if (request.idTokenHint) {
parameterBuilder.addIdTokenHint(request.idTokenHint);
}
if (request.state) {
parameterBuilder.addState(request.state);
}
if (request.logoutHint) {
parameterBuilder.addLogoutHint(request.logoutHint);
}
this.addExtraQueryParams(request, parameterBuilder);
return parameterBuilder.createQueryString();
}
private addExtraQueryParams(
request: CommonAuthorizationUrlRequest | CommonEndSessionRequest,
parameterBuilder: RequestParameterBuilder
) {
const hasRequestInstanceAware =
request.extraQueryParameters &&
request.extraQueryParameters.hasOwnProperty("instance_aware");
// Set instance_aware flag if config auth param is set
if (!hasRequestInstanceAware && this.config.authOptions.instanceAware) {
request.extraQueryParameters = request.extraQueryParameters || {};
request.extraQueryParameters["instance_aware"] = "true";
}
if (request.extraQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.extraQueryParameters
);
}
}
/**
* Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present.
* @param account
*/
private extractAccountSid(account: AccountInfo): string | null {
return account.idTokenClaims?.sid || null;
}
private extractLoginHint(account: AccountInfo): string | null {
return account.idTokenClaims?.login_hint || null;
}
}
+303
View File
@@ -0,0 +1,303 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientConfiguration,
buildClientConfiguration,
CommonClientConfiguration,
} from "../config/ClientConfiguration.js";
import {
INetworkModule,
NetworkRequestOptions,
} from "../network/INetworkModule.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { Authority } from "../authority/Authority.js";
import { Logger } from "../logger/Logger.js";
import { Constants, HeaderNames } from "../utils/Constants.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { version, name } from "../packageMetadata.js";
import { CcsCredential, CcsCredentialType } from "../account/CcsCredential.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import { createDiscoveredInstance } from "../authority/AuthorityFactory.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { ThrottlingUtils } from "../network/ThrottlingUtils.js";
import { AuthError } from "../error/AuthError.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { NetworkError } from "../error/NetworkError.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
/**
* Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow.
* @internal
*/
export abstract class BaseClient {
// Logger object
public logger: Logger;
// Application config
protected config: CommonClientConfiguration;
// Crypto Interface
protected cryptoUtils: ICrypto;
// Storage Interface
protected cacheManager: CacheManager;
// Network Interface
protected networkClient: INetworkModule;
// Server Telemetry Manager
protected serverTelemetryManager: ServerTelemetryManager | null;
// Default authority object
public authority: Authority;
// Performance telemetry client
protected performanceClient?: IPerformanceClient;
protected constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
// Set the configuration
this.config = buildClientConfiguration(configuration);
// Initialize the logger
this.logger = new Logger(this.config.loggerOptions, name, version);
// Initialize crypto
this.cryptoUtils = this.config.cryptoInterface;
// Initialize storage interface
this.cacheManager = this.config.storageInterface;
// Set the network interface
this.networkClient = this.config.networkInterface;
// Set TelemetryManager
this.serverTelemetryManager = this.config.serverTelemetryManager;
// set Authority
this.authority = this.config.authOptions.authority;
// set performance telemetry client
this.performanceClient = performanceClient;
}
/**
* Creates default headers for requests to token endpoint
*/
protected createTokenRequestHeaders(
ccsCred?: CcsCredential
): Record<string, string> {
const headers: Record<string, string> = {};
headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE;
if (!this.config.systemOptions.preventCorsPreflight && ccsCred) {
switch (ccsCred.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
ccsCred.credential
);
headers[
HeaderNames.CCS_HEADER
] = `Oid:${clientInfo.uid}@${clientInfo.utid}`;
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
headers[
HeaderNames.CCS_HEADER
] = `UPN: ${ccsCred.credential}`;
break;
}
}
return headers;
}
/**
* Http post to token endpoint
* @param tokenEndpoint
* @param queryString
* @param headers
* @param thumbprint
*/
protected async executePostToTokenEndpoint(
tokenEndpoint: string,
queryString: string,
headers: Record<string, string>,
thumbprint: RequestThumbprint,
correlationId: string,
queuedEvent?: string
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
if (queuedEvent) {
this.performanceClient?.addQueueMeasurement(
queuedEvent,
correlationId
);
}
const response =
await this.sendPostRequest<ServerAuthorizationTokenResponse>(
thumbprint,
tokenEndpoint,
{ body: queryString, headers: headers },
correlationId
);
if (
this.config.serverTelemetryManager &&
response.status < 500 &&
response.status !== 429
) {
// Telemetry data successfully logged by server, clear Telemetry cache
this.config.serverTelemetryManager.clearTelemetryCache();
}
return response;
}
/**
* Wraps sendPostRequestAsync with necessary preflight and postflight logic
* @param thumbprint - Request thumbprint for throttling
* @param tokenEndpoint - Endpoint to make the POST to
* @param options - Body and Headers to include on the POST request
* @param correlationId - CorrelationId for telemetry
*/
async sendPostRequest<T extends ServerAuthorizationTokenResponse>(
thumbprint: RequestThumbprint,
tokenEndpoint: string,
options: NetworkRequestOptions,
correlationId: string
): Promise<NetworkResponse<T>> {
ThrottlingUtils.preProcess(this.cacheManager, thumbprint);
let response;
try {
response = await invokeAsync(
this.networkClient.sendPostRequestAsync.bind(
this.networkClient
)<T>,
PerformanceEvents.NetworkClientSendPostRequestAsync,
this.logger,
this.performanceClient,
correlationId
)(tokenEndpoint, options);
const responseHeaders = response.headers || {};
this.performanceClient?.addFields(
{
refreshTokenSize: response.body.refresh_token?.length || 0,
httpVerToken:
responseHeaders[HeaderNames.X_MS_HTTP_VERSION] || "",
requestId:
responseHeaders[HeaderNames.X_MS_REQUEST_ID] || "",
},
correlationId
);
} catch (e) {
if (e instanceof NetworkError) {
const responseHeaders = e.responseHeaders;
if (responseHeaders) {
this.performanceClient?.addFields(
{
httpVerToken:
responseHeaders[
HeaderNames.X_MS_HTTP_VERSION
] || "",
requestId:
responseHeaders[HeaderNames.X_MS_REQUEST_ID] ||
"",
contentTypeHeader:
responseHeaders[HeaderNames.CONTENT_TYPE] ||
undefined,
contentLengthHeader:
responseHeaders[HeaderNames.CONTENT_LENGTH] ||
undefined,
httpStatus: e.httpStatus,
},
correlationId
);
}
throw e.error;
}
if (e instanceof AuthError) {
throw e;
} else {
throw createClientAuthError(ClientAuthErrorCodes.networkError);
}
}
ThrottlingUtils.postProcess(this.cacheManager, thumbprint, response);
return response;
}
/**
* Updates the authority object of the client. Endpoint discovery must be completed.
* @param updatedAuthority
*/
async updateAuthority(
cloudInstanceHostname: string,
correlationId: string
): Promise<void> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.UpdateTokenEndpointAuthority,
correlationId
);
const cloudInstanceAuthorityUri = `https://${cloudInstanceHostname}/${this.authority.tenant}/`;
const cloudInstanceAuthority = await createDiscoveredInstance(
cloudInstanceAuthorityUri,
this.networkClient,
this.cacheManager,
this.authority.options,
this.logger,
correlationId,
this.performanceClient
);
this.authority = cloudInstanceAuthority;
}
/**
* Creates query string for the /token request
* @param request
*/
createTokenQueryParameters(request: BaseAuthRequest): string {
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenQueryParameters
);
}
parameterBuilder.addCorrelationId(request.correlationId);
return parameterBuilder.createQueryString();
}
}
+501
View File
@@ -0,0 +1,501 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { BaseClient } from "./BaseClient.js";
import { CommonRefreshTokenRequest } from "../request/CommonRefreshTokenRequest.js";
import { Authority } from "../authority/Authority.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import {
GrantType,
AuthenticationScheme,
Errors,
HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { StringUtils } from "../utils/StringUtils.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { CommonSilentFlowRequest } from "../request/CommonSilentFlowRequest.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
import { ServerError } from "../error/ServerError.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { UrlString } from "../url/UrlString.js";
import { CcsCredentialType } from "../account/CcsCredential.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import {
InteractionRequiredAuthError,
InteractionRequiredAuthErrorCodes,
createInteractionRequiredAuthError,
} from "../error/InteractionRequiredAuthError.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { invoke, invokeAsync } from "../utils/FunctionWrappers.js";
import { generateCredentialKey } from "../cache/utils/CacheHelpers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
const DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS = 300; // 5 Minutes
/**
* OAuth2.0 refresh token client
* @internal
*/
export class RefreshTokenClient extends BaseClient {
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
}
public async acquireToken(
request: CommonRefreshTokenRequest
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireToken,
request.correlationId
);
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.RefreshTokenClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.authority);
// Retrieve requestId from response headers
const requestId = response.headers?.[HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(response.body);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
undefined,
undefined,
true,
request.forceCache,
requestId
);
}
/**
* Gets cached refresh token and attaches to request, then calls acquireToken API
* @param request
*/
public async acquireTokenByRefreshToken(
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
// Cannot renew token if no request object is given.
if (!request) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.tokenRequestEmpty
);
}
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireTokenByRefreshToken,
request.correlationId
);
// We currently do not support silent flow for account === null use cases; This will be revisited for confidential flow usecases
if (!request.account) {
throw createClientAuthError(
ClientAuthErrorCodes.noAccountInSilentRequest
);
}
// try checking if FOCI is enabled for the given application
const isFOCI = this.cacheManager.isAppMetadataFOCI(
request.account.environment
);
// if the app is part of the family, retrive a Family refresh token if present and make a refreshTokenRequest
if (isFOCI) {
try {
return await invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, true);
} catch (e) {
const noFamilyRTInCache =
e instanceof InteractionRequiredAuthError &&
e.errorCode ===
InteractionRequiredAuthErrorCodes.noTokensFound;
const clientMismatchErrorWithFamilyRT =
e instanceof ServerError &&
e.errorCode === Errors.INVALID_GRANT_ERROR &&
e.subError === Errors.CLIENT_MISMATCH_ERROR;
// if family Refresh Token (FRT) cache acquisition fails or if client_mismatch error is seen with FRT, reattempt with application Refresh Token (ART)
if (noFamilyRTInCache || clientMismatchErrorWithFamilyRT) {
return invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, false);
// throw in all other cases
} else {
throw e;
}
}
}
// fall back to application refresh token acquisition
return invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, false);
}
/**
* makes a network call to acquire tokens by exchanging RefreshToken available in userCache; throws if refresh token is not cached
* @param request
*/
private async acquireTokenWithCachedRefreshToken(
request: CommonSilentFlowRequest,
foci: boolean
) {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
request.correlationId
);
// fetches family RT or application RT based on FOCI value
const refreshToken = invoke(
this.cacheManager.getRefreshToken.bind(this.cacheManager),
PerformanceEvents.CacheManagerGetRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(
request.account,
foci,
undefined,
this.performanceClient,
request.correlationId
);
if (!refreshToken) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.noTokensFound
);
}
if (
refreshToken.expiresOn &&
TimeUtils.isTokenExpired(
refreshToken.expiresOn,
request.refreshTokenExpirationOffsetSeconds ||
DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS
)
) {
this.performanceClient?.addFields(
{ rtExpiresOnMs: Number(refreshToken.expiresOn) },
request.correlationId
);
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.refreshTokenExpired
);
}
// attach cached RT size to the current measurement
const refreshTokenRequest: CommonRefreshTokenRequest = {
...request,
refreshToken: refreshToken.secret,
authenticationScheme:
request.authenticationScheme || AuthenticationScheme.BEARER,
ccsCredential: {
credential: request.account.homeAccountId,
type: CcsCredentialType.HOME_ACCOUNT_ID,
},
};
try {
return await invokeAsync(
this.acquireToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireToken,
this.logger,
this.performanceClient,
request.correlationId
)(refreshTokenRequest);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
this.performanceClient?.addFields(
{ rtExpiresOnMs: Number(refreshToken.expiresOn) },
request.correlationId
);
if (e.subError === InteractionRequiredAuthErrorCodes.badToken) {
// Remove bad refresh token from cache
this.logger.verbose(
"acquireTokenWithRefreshToken: bad refresh token, removing from cache"
);
const badRefreshTokenKey =
generateCredentialKey(refreshToken);
this.cacheManager.removeRefreshToken(badRefreshTokenKey);
}
}
throw e;
}
}
/**
* Constructs the network message and makes a NW call to the underlying secure token service
* @param request
* @param authority
*/
private async executeTokenRequest(
request: CommonRefreshTokenRequest,
authority: Authority
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientExecuteTokenRequest,
request.correlationId
);
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await invokeAsync(
this.createTokenRequestBody.bind(this),
PerformanceEvents.RefreshTokenClientCreateTokenRequestBody,
this.logger,
this.performanceClient,
request.correlationId
)(request);
const headers: Record<string, string> = this.createTokenRequestHeaders(
request.ccsCredential
);
const thumbprint: RequestThumbprint = {
clientId:
request.tokenBodyParameters?.clientId ||
this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return invokeAsync(
this.executePostToTokenEndpoint.bind(this),
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint
);
}
/**
* Helper function to create the token request body
* @param request
*/
private async createTokenRequestBody(
request: CommonRefreshTokenRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientCreateTokenRequestBody,
request.correlationId
);
const correlationId = request.correlationId;
const parameterBuilder = new RequestParameterBuilder(
correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
if (request.redirectUri) {
parameterBuilder.addRedirectUri(request.redirectUri);
}
parameterBuilder.addScopes(
request.scopes,
true,
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes
);
parameterBuilder.addGrantType(GrantType.REFRESH_TOKEN_GRANT);
parameterBuilder.addClientInfo();
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
parameterBuilder.addRefreshToken(request.refreshToken);
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils,
this.performanceClient
);
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
// SPA PoP requires full Base64Url encoded req_cnf string (unhashed)
parameterBuilder.addPopToken(reqCnfData);
} else if (request.authenticationScheme === AuthenticationScheme.SSH) {
if (request.sshJwk) {
parameterBuilder.addSshJwk(request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
if (
this.config.systemOptions.preventCorsPreflight &&
request.ccsCredential
) {
switch (request.ccsCredential.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.ccsCredential.credential
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
parameterBuilder.addCcsUpn(
request.ccsCredential.credential
);
break;
}
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenBodyParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenBodyParameters
);
}
return parameterBuilder.createQueryString();
}
}
+211
View File
@@ -0,0 +1,211 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseClient } from "./BaseClient.js";
import { ClientConfiguration } from "../config/ClientConfiguration.js";
import { CommonSilentFlowRequest } from "../request/CommonSilentFlowRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { CacheRecord } from "../cache/entities/CacheRecord.js";
import { CacheOutcome } from "../utils/Constants.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { StringUtils } from "../utils/StringUtils.js";
import { checkMaxAge, extractTokenClaims } from "../account/AuthToken.js";
import { TokenClaims } from "../account/TokenClaims.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { getTenantFromAuthorityString } from "../authority/Authority.js";
/** @internal */
export class SilentFlowClient extends BaseClient {
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
}
/**
* Retrieves token from cache or throws an error if it must be refreshed.
* @param request
*/
async acquireCachedToken(
request: CommonSilentFlowRequest
): Promise<[AuthenticationResult, CacheOutcome]> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.SilentFlowClientAcquireCachedToken,
request.correlationId
);
let lastCacheOutcome: CacheOutcome = CacheOutcome.NOT_APPLICABLE;
if (
request.forceRefresh ||
(!this.config.cacheOptions.claimsBasedCachingEnabled &&
!StringUtils.isEmptyObj(request.claims))
) {
// Must refresh due to present force_refresh flag.
this.setCacheOutcome(
CacheOutcome.FORCE_REFRESH_OR_CLAIMS,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
}
// We currently do not support silent flow for account === null use cases; This will be revisited for confidential flow usecases
if (!request.account) {
throw createClientAuthError(
ClientAuthErrorCodes.noAccountInSilentRequest
);
}
const requestTenantId =
request.account.tenantId ||
getTenantFromAuthorityString(request.authority);
const tokenKeys = this.cacheManager.getTokenKeys();
const cachedAccessToken = this.cacheManager.getAccessToken(
request.account,
request,
tokenKeys,
requestTenantId,
this.performanceClient,
request.correlationId
);
if (!cachedAccessToken) {
// must refresh due to non-existent access_token
this.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
} else if (
TimeUtils.wasClockTurnedBack(cachedAccessToken.cachedAt) ||
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.systemOptions.tokenRenewalOffsetSeconds
)
) {
// must refresh due to the expires_in value
this.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
} else if (
cachedAccessToken.refreshOn &&
TimeUtils.isTokenExpired(cachedAccessToken.refreshOn, 0)
) {
// must refresh (in the background) due to the refresh_in value
lastCacheOutcome = CacheOutcome.PROACTIVELY_REFRESHED;
// don't throw ClientAuthError.createRefreshRequiredError(), return cached token instead
}
const environment =
request.authority || this.authority.getPreferredCache();
const cacheRecord: CacheRecord = {
account: this.cacheManager.readAccountFromCache(request.account),
accessToken: cachedAccessToken,
idToken: this.cacheManager.getIdToken(
request.account,
tokenKeys,
requestTenantId,
this.performanceClient,
request.correlationId
),
refreshToken: null,
appMetadata:
this.cacheManager.readAppMetadataFromCache(environment),
};
this.setCacheOutcome(lastCacheOutcome, request.correlationId);
if (this.config.serverTelemetryManager) {
this.config.serverTelemetryManager.incrementCacheHits();
}
return [
await invokeAsync(
this.generateResultFromCacheRecord.bind(this),
PerformanceEvents.SilentFlowClientGenerateResultFromCacheRecord,
this.logger,
this.performanceClient,
request.correlationId
)(cacheRecord, request),
lastCacheOutcome,
];
}
private setCacheOutcome(
cacheOutcome: CacheOutcome,
correlationId: string
): void {
this.serverTelemetryManager?.setCacheOutcome(cacheOutcome);
this.performanceClient?.addFields(
{
cacheOutcome: cacheOutcome,
},
correlationId
);
if (cacheOutcome !== CacheOutcome.NOT_APPLICABLE) {
this.logger.info(
`Token refresh is required due to cache outcome: ${cacheOutcome}`
);
}
}
/**
* Helper function to build response object from the CacheRecord
* @param cacheRecord
*/
private async generateResultFromCacheRecord(
cacheRecord: CacheRecord,
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.SilentFlowClientGenerateResultFromCacheRecord,
request.correlationId
);
let idTokenClaims: TokenClaims | undefined;
if (cacheRecord.idToken) {
idTokenClaims = extractTokenClaims(
cacheRecord.idToken.secret,
this.config.cryptoInterface.base64Decode
);
}
// token max_age check
if (request.maxAge || request.maxAge === 0) {
const authTime = idTokenClaims?.auth_time;
if (!authTime) {
throw createClientAuthError(
ClientAuthErrorCodes.authTimeNotFound
);
}
checkMaxAge(authTime, request.maxAge);
}
return ResponseHandler.generateAuthenticationResult(
this.cryptoUtils,
this.authority,
cacheRecord,
true,
request,
idTokenClaims
);
}
}