Estructura inicial del proyecto

This commit is contained in:
2026-06-02 16:57:08 +00:00
commit 8b306b9afc
9864 changed files with 1435687 additions and 0 deletions

View File

@ -0,0 +1,606 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js";
import { Authority } from "../authority/Authority.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import * as UrlUtils from "../utils/UrlUtils.js";
import * as Constants from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
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 {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { UrlString } from "../url/UrlString.js";
import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import {
buildClientInfoFromHomeAccountId,
buildClientInfo,
} from "../account/ClientInfo.js";
import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
import { getRequestThumbprint } from "../network/RequestThumbprint.js";
import {
createTokenQueryParameters,
createTokenRequestHeaders,
executePostToTokenEndpoint,
} from "../protocol/Token.js";
import { createDiscoveredInstance } from "../authority/AuthorityFactory.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { Logger } from "../logger/Logger.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { CacheManager } from "../cache/CacheManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { version, name } from "../packageMetadata.js";
/**
* Oauth2.0 Authorization Code client
* @internal
*/
export class AuthorizationCodeClient {
// Flag to indicate if client is for hybrid spa auth code redemption
protected includeRedirectUri: boolean = true;
private oidcDefaultScopes;
// 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;
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;
this.oidcDefaultScopes =
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes;
}
/**
* 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,
apiId: number,
authCodePayload?: AuthorizationCodePayload
): Promise<AuthenticationResult> {
if (!request.code) {
throw createClientAuthError(
ClientAuthErrorCodes.requestCannotBeMade
);
}
// Check for new cloud instance
if (authCodePayload && authCodePayload.cloud_instance_host_name) {
await invokeAsync(
this.updateTokenEndpointAuthority.bind(this),
PerformanceEvents.UpdateTokenEndpointAuthority,
this.logger,
this.performanceClient,
request.correlationId
)(authCodePayload.cloud_instance_host_name, request.correlationId);
}
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.AuthClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(this.authority, request, this.serverTelemetryManager);
// Retrieve requestId from response headers
const requestId =
response.headers?.[Constants.HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.performanceClient,
this.config.serializableCache,
this.config.persistencePlugin
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(
response.body,
request.correlationId
);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
apiId,
authCodePayload,
undefined,
undefined,
undefined,
requestId
);
}
/**
* 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,
serverTelemetryManager: ServerTelemetryManager | null
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const queryParametersString = createTokenQueryParameters(
request,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri,
this.performanceClient
);
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}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
`Could not parse client info for CCS Header: '${e}'`,
request.correlationId
);
}
}
const headers: Record<string, string> = createTokenRequestHeaders(
this.logger,
this.config.systemOptions.preventCorsPreflight,
ccsCredential || request.ccsCredential
);
const thumbprint = getRequestThumbprint(
this.config.authOptions.clientId,
request
);
return invokeAsync(
executePostToTokenEndpoint,
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
this.cacheManager,
this.networkClient,
this.logger,
this.performanceClient,
serverTelemetryManager
);
}
/**
* Generates a map for all the params to be sent to the service
* @param request
*/
private async createTokenRequestBody(
request: CommonAuthorizationCodeRequest
): Promise<string> {
const parameters = new Map<string, string>();
RequestParameterBuilder.addClientId(
parameters,
request.embeddedClientId ||
request.extraParameters?.[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
if (!request.redirectUri) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.redirectUriEmpty
);
}
} else {
// Validate and include redirect uri
RequestParameterBuilder.addRedirectUri(
parameters,
request.redirectUri
);
}
// Add scope array, parameter builder will add default scopes and dedupe
RequestParameterBuilder.addScopes(
parameters,
request.scopes,
true,
this.oidcDefaultScopes
);
RequestParameterBuilder.addResource(parameters, request.resource);
// add code: user set, not validated
RequestParameterBuilder.addAuthorizationCode(parameters, request.code);
// Add library metadata
RequestParameterBuilder.addLibraryInfo(
parameters,
this.config.libraryInfo
);
RequestParameterBuilder.addApplicationTelemetry(
parameters,
this.config.telemetry.application
);
RequestParameterBuilder.addThrottling(parameters);
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
RequestParameterBuilder.addServerTelemetry(
parameters,
this.serverTelemetryManager
);
}
// add code_verifier if passed
if (request.codeVerifier) {
RequestParameterBuilder.addCodeVerifier(
parameters,
request.codeVerifier
);
}
if (this.config.clientCredentials.clientSecret) {
RequestParameterBuilder.addClientSecret(
parameters,
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
RequestParameterBuilder.addClientAssertion(
parameters,
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
RequestParameterBuilder.addClientAssertionType(
parameters,
clientAssertion.assertionType
);
}
RequestParameterBuilder.addGrantType(
parameters,
Constants.GrantType.AUTHORIZATION_CODE_GRANT
);
RequestParameterBuilder.addClientInfo(parameters);
if (
request.authenticationScheme === Constants.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)
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
} else if (
request.authenticationScheme === Constants.AuthenticationScheme.SSH
) {
if (request.sshJwk) {
RequestParameterBuilder.addSshJwk(parameters, request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
let ccsCred: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCred = {
credential: `${clientInfo.uid}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
`Could not parse client info for CCS Header: '${e}'`,
request.correlationId
);
}
} 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
);
RequestParameterBuilder.addCcsOid(
parameters,
clientInfo
);
} catch (e) {
this.logger.verbose(
`Could not parse home account ID for CCS Header: '${e}'`,
request.correlationId
);
}
break;
case CcsCredentialType.UPN:
RequestParameterBuilder.addCcsUpn(
parameters,
ccsCred.credential
);
break;
}
}
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri
);
}
if (request.extraParameters) {
RequestParameterBuilder.addExtraParameters(
parameters,
request.extraParameters
);
}
// Add hybrid spa parameters if not already provided
if (
request.enableSpaAuthorizationCode &&
(!request.extraParameters ||
!request.extraParameters[AADServerParamKeys.RETURN_SPA_CODE])
) {
RequestParameterBuilder.addExtraParameters(parameters, {
[AADServerParamKeys.RETURN_SPA_CODE]: "1",
});
}
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
this.performanceClient
);
RequestParameterBuilder.addClaims(
parameters,
request.claims,
this.config.authOptions.clientCapabilities,
request.skipBrokerClaims
);
return UrlUtils.mapToQueryString(parameters);
}
/**
* This API validates the `EndSessionRequest` and creates a URL
* @param request
*/
private createLogoutUrlQueryString(
request: CommonEndSessionRequest
): string {
const parameters = new Map<string, string>();
if (request.postLogoutRedirectUri) {
RequestParameterBuilder.addPostLogoutRedirectUri(
parameters,
request.postLogoutRedirectUri
);
}
if (request.correlationId) {
RequestParameterBuilder.addCorrelationId(
parameters,
request.correlationId
);
}
if (request.idTokenHint) {
RequestParameterBuilder.addIdTokenHint(
parameters,
request.idTokenHint
);
}
if (request.state) {
RequestParameterBuilder.addState(parameters, request.state);
}
if (request.logoutHint) {
RequestParameterBuilder.addLogoutHint(
parameters,
request.logoutHint
);
}
if (request.extraQueryParameters) {
RequestParameterBuilder.addExtraParameters(
parameters,
request.extraQueryParameters
);
}
if (this.config.authOptions.instanceAware) {
RequestParameterBuilder.addInstanceAware(parameters);
}
return UrlUtils.mapToQueryString(parameters);
}
/**
* Updates the authority to the cloud instance provided in the authorization response
* @param cloudInstanceHostName - cloud instance host name from authorization code payload
* @param correlationId - request correlation id
*/
private async updateTokenEndpointAuthority(
cloudInstanceHostName: string,
correlationId: string
): Promise<void> {
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;
}
}

View File

@ -0,0 +1,564 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { CommonRefreshTokenRequest } from "../request/CommonRefreshTokenRequest.js";
import { Authority } from "../authority/Authority.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import * as UrlUtils from "../utils/UrlUtils.js";
import * as Constants 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 { 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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { invoke, invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
import { getRequestThumbprint } from "../network/RequestThumbprint.js";
import {
createTokenQueryParameters,
createTokenRequestHeaders,
executePostToTokenEndpoint,
} from "../protocol/Token.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { Logger } from "../logger/Logger.js";
import { version, name } from "../packageMetadata.js";
const DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS = 300; // 5 Minutes
/**
* OAuth2.0 refresh token client
* @internal
*/
export class RefreshTokenClient {
// 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;
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;
}
public async acquireToken(
request: CommonRefreshTokenRequest,
apiId: number
): Promise<AuthenticationResult> {
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?.[Constants.HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.performanceClient,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(
response.body,
request.correlationId
);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
apiId,
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,
apiId: number
): Promise<AuthenticationResult> {
// Cannot renew token if no request object is given.
if (!request) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.tokenRequestEmpty
);
}
// 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,
request.correlationId
);
// 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, apiId);
} catch (e) {
const noFamilyRTInCache =
e instanceof InteractionRequiredAuthError &&
e.errorCode ===
InteractionRequiredAuthErrorCodes.noTokensFound;
const clientMismatchErrorWithFamilyRT =
e instanceof ServerError &&
e.errorCode === Constants.INVALID_GRANT_ERROR &&
e.subError === Constants.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, apiId);
// 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, apiId);
}
/**
* 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,
apiId: number
) {
// 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, request.correlationId, undefined);
if (!refreshToken) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.noTokensFound
);
}
if (refreshToken.expiresOn) {
const offset =
request.refreshTokenExpirationOffsetSeconds ||
DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS;
this.performanceClient?.addFields(
{
cacheRtExpiresOnSeconds: Number(refreshToken.expiresOn),
rtOffsetSeconds: offset,
},
request.correlationId
);
if (TimeUtils.isTokenExpired(refreshToken.expiresOn, offset)) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.refreshTokenExpired
);
}
}
// attach cached RT size to the current measurement
const refreshTokenRequest: CommonRefreshTokenRequest = {
...request,
refreshToken: refreshToken.secret,
authenticationScheme:
request.authenticationScheme ||
Constants.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, apiId);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
if (e.subError === InteractionRequiredAuthErrorCodes.badToken) {
// Remove bad refresh token from cache
this.logger.verbose(
"acquireTokenWithRefreshToken: bad refresh token, removing from cache",
request.correlationId
);
const badRefreshTokenKey =
this.cacheManager.generateCredentialKey(refreshToken);
this.cacheManager.removeRefreshToken(
badRefreshTokenKey,
request.correlationId
);
}
}
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>> {
const queryParametersString = createTokenQueryParameters(
request,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri,
this.performanceClient
);
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> = createTokenRequestHeaders(
this.logger,
this.config.systemOptions.preventCorsPreflight,
request.ccsCredential
);
const thumbprint = getRequestThumbprint(
this.config.authOptions.clientId,
request
);
return invokeAsync(
executePostToTokenEndpoint,
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
this.cacheManager,
this.networkClient,
this.logger,
this.performanceClient,
this.serverTelemetryManager
);
}
/**
* Helper function to create the token request body
* @param request
*/
private async createTokenRequestBody(
request: CommonRefreshTokenRequest
): Promise<string> {
const parameters = new Map<string, string>();
RequestParameterBuilder.addClientId(
parameters,
request.embeddedClientId ||
request.extraParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
if (request.redirectUri) {
RequestParameterBuilder.addRedirectUri(
parameters,
request.redirectUri
);
}
RequestParameterBuilder.addScopes(
parameters,
request.scopes,
true,
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes
);
RequestParameterBuilder.addGrantType(
parameters,
Constants.GrantType.REFRESH_TOKEN_GRANT
);
RequestParameterBuilder.addClientInfo(parameters);
RequestParameterBuilder.addLibraryInfo(
parameters,
this.config.libraryInfo
);
RequestParameterBuilder.addApplicationTelemetry(
parameters,
this.config.telemetry.application
);
RequestParameterBuilder.addThrottling(parameters);
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
RequestParameterBuilder.addServerTelemetry(
parameters,
this.serverTelemetryManager
);
}
RequestParameterBuilder.addRefreshToken(
parameters,
request.refreshToken
);
if (this.config.clientCredentials.clientSecret) {
RequestParameterBuilder.addClientSecret(
parameters,
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
RequestParameterBuilder.addClientAssertion(
parameters,
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
RequestParameterBuilder.addClientAssertionType(
parameters,
clientAssertion.assertionType
);
}
if (
request.authenticationScheme === Constants.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)
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
} else if (
request.authenticationScheme === Constants.AuthenticationScheme.SSH
) {
if (request.sshJwk) {
RequestParameterBuilder.addSshJwk(parameters, request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
this.config.systemOptions.preventCorsPreflight &&
request.ccsCredential
) {
switch (request.ccsCredential.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.ccsCredential.credential
);
RequestParameterBuilder.addCcsOid(
parameters,
clientInfo
);
} catch (e) {
this.logger.verbose(
`Could not parse home account ID for CCS Header: '${e}'`,
request.correlationId
);
}
break;
case CcsCredentialType.UPN:
RequestParameterBuilder.addCcsUpn(
parameters,
request.ccsCredential.credential
);
break;
}
}
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri
);
}
if (request.extraParameters) {
RequestParameterBuilder.addExtraParameters(parameters, {
...request.extraParameters,
});
}
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
this.performanceClient
);
RequestParameterBuilder.addClaims(
parameters,
request.claims,
this.config.authOptions.clientCapabilities,
request.skipBrokerClaims
);
return UrlUtils.mapToQueryString(parameters);
}
}

View File

@ -0,0 +1,272 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
} 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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import {
Authority,
getTenantFromAuthorityString,
} from "../authority/Authority.js";
import { Logger } from "../logger/Logger.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { CacheManager } from "../cache/CacheManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { version, name } from "../packageMetadata.js";
/** @internal */
export class SilentFlowClient {
// 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;
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;
}
/**
* Retrieves token from cache or throws an error if it must be refreshed.
* @param request
*/
async acquireCachedToken(
request: CommonSilentFlowRequest
): Promise<[AuthenticationResult, CacheOutcome]> {
let lastCacheOutcome: CacheOutcome = CacheOutcome.NOT_APPLICABLE;
if (request.forceRefresh || !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
);
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 (request.resource) {
// cached access token must have a resource that matches the request resource for MCP scenarios
if (cachedAccessToken.resource !== request.resource) {
this.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN,
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.getAccount(
this.cacheManager.generateAccountKey(request.account),
request.correlationId
),
accessToken: cachedAccessToken,
idToken: this.cacheManager.getIdToken(
request.account,
request.correlationId,
tokenKeys,
requestTenantId
),
refreshToken: null,
appMetadata: this.cacheManager.readAppMetadataFromCache(
environment,
request.correlationId
),
};
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}'`,
correlationId
);
}
}
/**
* Helper function to build response object from the CacheRecord
* @param cacheRecord
*/
private async generateResultFromCacheRecord(
cacheRecord: CacheRecord,
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
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,
this.performanceClient,
idTokenClaims
);
}
}