211 lines
10 KiB
JavaScript
211 lines
10 KiB
JavaScript
|
/*! @azure/msal-node v2.5.1 2023-11-07 */
|
||
|
'use strict';
|
||
|
import { BaseClient, ScopeSet, CacheOutcome, createClientAuthError, ClientAuthErrorCodes, TimeUtils, AuthToken, ResponseHandler, AuthenticationScheme, CredentialType, UrlString, RequestParameterBuilder, GrantType, AADServerParamKeys, Constants } from '@azure/msal-common';
|
||
|
import { EncodingUtils } from '../utils/EncodingUtils.mjs';
|
||
|
|
||
|
/*
|
||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
* Licensed under the MIT License.
|
||
|
*/
|
||
|
/**
|
||
|
* On-Behalf-Of client
|
||
|
*/
|
||
|
class OnBehalfOfClient extends BaseClient {
|
||
|
constructor(configuration) {
|
||
|
super(configuration);
|
||
|
}
|
||
|
/**
|
||
|
* Public API to acquire tokens with on behalf of flow
|
||
|
* @param request
|
||
|
*/
|
||
|
async acquireToken(request) {
|
||
|
this.scopeSet = new ScopeSet(request.scopes || []);
|
||
|
// generate the user_assertion_hash for OBOAssertion
|
||
|
this.userAssertionHash = await this.cryptoUtils.hashString(request.oboAssertion);
|
||
|
if (request.skipCache) {
|
||
|
return await this.executeTokenRequest(request, this.authority, this.userAssertionHash);
|
||
|
}
|
||
|
try {
|
||
|
return await this.getCachedAuthenticationResult(request);
|
||
|
}
|
||
|
catch (e) {
|
||
|
// Any failure falls back to interactive request, once we implement distributed cache, we plan to handle `createRefreshRequiredError` to refresh using the RT
|
||
|
return await this.executeTokenRequest(request, this.authority, this.userAssertionHash);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* look up cache for tokens
|
||
|
* Find idtoken in the cache
|
||
|
* Find accessToken based on user assertion and account info in the cache
|
||
|
* Please note we are not yet supported OBO tokens refreshed with long lived RT. User will have to send a new assertion if the current access token expires
|
||
|
* This is to prevent security issues when the assertion changes over time, however, longlived RT helps retaining the session
|
||
|
* @param request
|
||
|
*/
|
||
|
async getCachedAuthenticationResult(request) {
|
||
|
// look in the cache for the access_token which matches the incoming_assertion
|
||
|
const cachedAccessToken = this.readAccessTokenFromCacheForOBO(this.config.authOptions.clientId, request);
|
||
|
if (!cachedAccessToken) {
|
||
|
// Must refresh due to non-existent access_token.
|
||
|
this.serverTelemetryManager?.setCacheOutcome(CacheOutcome.NO_CACHED_ACCESS_TOKEN);
|
||
|
this.logger.info("SilentFlowClient:acquireCachedToken - No access token found in cache for the given properties.");
|
||
|
throw createClientAuthError(ClientAuthErrorCodes.tokenRefreshRequired);
|
||
|
}
|
||
|
else if (TimeUtils.isTokenExpired(cachedAccessToken.expiresOn, this.config.systemOptions.tokenRenewalOffsetSeconds)) {
|
||
|
// Access token expired, will need to renewed
|
||
|
this.serverTelemetryManager?.setCacheOutcome(CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED);
|
||
|
this.logger.info(`OnbehalfofFlow:getCachedAuthenticationResult - Cached access token is expired or will expire within ${this.config.systemOptions.tokenRenewalOffsetSeconds} seconds.`);
|
||
|
throw createClientAuthError(ClientAuthErrorCodes.tokenRefreshRequired);
|
||
|
}
|
||
|
// fetch the idToken from cache
|
||
|
const cachedIdToken = this.readIdTokenFromCacheForOBO(cachedAccessToken.homeAccountId);
|
||
|
let idTokenClaims;
|
||
|
let cachedAccount = null;
|
||
|
if (cachedIdToken) {
|
||
|
idTokenClaims = AuthToken.extractTokenClaims(cachedIdToken.secret, EncodingUtils.base64Decode);
|
||
|
const localAccountId = idTokenClaims.oid || idTokenClaims.sub;
|
||
|
const accountInfo = {
|
||
|
homeAccountId: cachedIdToken.homeAccountId,
|
||
|
environment: cachedIdToken.environment,
|
||
|
tenantId: cachedIdToken.realm,
|
||
|
username: Constants.EMPTY_STRING,
|
||
|
localAccountId: localAccountId || Constants.EMPTY_STRING,
|
||
|
};
|
||
|
cachedAccount = this.cacheManager.readAccountFromCache(accountInfo);
|
||
|
}
|
||
|
// increment telemetry cache hit counter
|
||
|
if (this.config.serverTelemetryManager) {
|
||
|
this.config.serverTelemetryManager.incrementCacheHits();
|
||
|
}
|
||
|
return await ResponseHandler.generateAuthenticationResult(this.cryptoUtils, this.authority, {
|
||
|
account: cachedAccount,
|
||
|
accessToken: cachedAccessToken,
|
||
|
idToken: cachedIdToken,
|
||
|
refreshToken: null,
|
||
|
appMetadata: null,
|
||
|
}, true, request, idTokenClaims);
|
||
|
}
|
||
|
/**
|
||
|
* read idtoken from cache, this is a specific implementation for OBO as the requirements differ from a generic lookup in the cacheManager
|
||
|
* Certain use cases of OBO flow do not expect an idToken in the cache/or from the service
|
||
|
* @param atHomeAccountId {string}
|
||
|
*/
|
||
|
readIdTokenFromCacheForOBO(atHomeAccountId) {
|
||
|
const idTokenFilter = {
|
||
|
homeAccountId: atHomeAccountId,
|
||
|
environment: this.authority.canonicalAuthorityUrlComponents.HostNameAndPort,
|
||
|
credentialType: CredentialType.ID_TOKEN,
|
||
|
clientId: this.config.authOptions.clientId,
|
||
|
realm: this.authority.tenant,
|
||
|
};
|
||
|
const idTokens = this.cacheManager.getIdTokensByFilter(idTokenFilter);
|
||
|
// When acquiring a token on behalf of an application, there might not be an id token in the cache
|
||
|
if (idTokens.length < 1) {
|
||
|
return null;
|
||
|
}
|
||
|
return idTokens[0];
|
||
|
}
|
||
|
/**
|
||
|
* Fetches the cached access token based on incoming assertion
|
||
|
* @param clientId
|
||
|
* @param request
|
||
|
* @param userAssertionHash
|
||
|
*/
|
||
|
readAccessTokenFromCacheForOBO(clientId, request) {
|
||
|
const authScheme = request.authenticationScheme || AuthenticationScheme.BEARER;
|
||
|
/*
|
||
|
* Distinguish between Bearer and PoP/SSH token cache types
|
||
|
* Cast to lowercase to handle "bearer" from ADFS
|
||
|
*/
|
||
|
const credentialType = authScheme &&
|
||
|
authScheme.toLowerCase() !==
|
||
|
AuthenticationScheme.BEARER.toLowerCase()
|
||
|
? CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME
|
||
|
: CredentialType.ACCESS_TOKEN;
|
||
|
const accessTokenFilter = {
|
||
|
credentialType: credentialType,
|
||
|
clientId,
|
||
|
target: ScopeSet.createSearchScopes(this.scopeSet.asArray()),
|
||
|
tokenType: authScheme,
|
||
|
keyId: request.sshKid,
|
||
|
requestedClaimsHash: request.requestedClaimsHash,
|
||
|
userAssertionHash: this.userAssertionHash,
|
||
|
};
|
||
|
const accessTokens = this.cacheManager.getAccessTokensByFilter(accessTokenFilter);
|
||
|
const numAccessTokens = accessTokens.length;
|
||
|
if (numAccessTokens < 1) {
|
||
|
return null;
|
||
|
}
|
||
|
else if (numAccessTokens > 1) {
|
||
|
throw createClientAuthError(ClientAuthErrorCodes.multipleMatchingTokens);
|
||
|
}
|
||
|
return accessTokens[0];
|
||
|
}
|
||
|
/**
|
||
|
* Make a network call to the server requesting credentials
|
||
|
* @param request
|
||
|
* @param authority
|
||
|
*/
|
||
|
async executeTokenRequest(request, authority, userAssertionHash) {
|
||
|
const queryParametersString = this.createTokenQueryParameters(request);
|
||
|
const endpoint = UrlString.appendQueryString(authority.tokenEndpoint, queryParametersString);
|
||
|
const requestBody = this.createTokenRequestBody(request);
|
||
|
const headers = this.createTokenRequestHeaders();
|
||
|
const thumbprint = {
|
||
|
clientId: this.config.authOptions.clientId,
|
||
|
authority: request.authority,
|
||
|
scopes: request.scopes,
|
||
|
claims: request.claims,
|
||
|
authenticationScheme: request.authenticationScheme,
|
||
|
resourceRequestMethod: request.resourceRequestMethod,
|
||
|
resourceRequestUri: request.resourceRequestUri,
|
||
|
shrClaims: request.shrClaims,
|
||
|
sshKid: request.sshKid,
|
||
|
};
|
||
|
const reqTimestamp = TimeUtils.nowSeconds();
|
||
|
const response = await this.executePostToTokenEndpoint(endpoint, requestBody, headers, thumbprint, request.correlationId);
|
||
|
const responseHandler = new ResponseHandler(this.config.authOptions.clientId, this.cacheManager, this.cryptoUtils, this.logger, this.config.serializableCache, this.config.persistencePlugin);
|
||
|
responseHandler.validateTokenResponse(response.body);
|
||
|
const tokenResponse = await responseHandler.handleServerTokenResponse(response.body, this.authority, reqTimestamp, request, undefined, userAssertionHash);
|
||
|
return tokenResponse;
|
||
|
}
|
||
|
/**
|
||
|
* generate a server request in accepable format
|
||
|
* @param request
|
||
|
*/
|
||
|
createTokenRequestBody(request) {
|
||
|
const parameterBuilder = new RequestParameterBuilder();
|
||
|
parameterBuilder.addClientId(this.config.authOptions.clientId);
|
||
|
parameterBuilder.addScopes(request.scopes);
|
||
|
parameterBuilder.addGrantType(GrantType.JWT_BEARER);
|
||
|
parameterBuilder.addClientInfo();
|
||
|
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
|
||
|
parameterBuilder.addApplicationTelemetry(this.config.telemetry.application);
|
||
|
parameterBuilder.addThrottling();
|
||
|
if (this.serverTelemetryManager) {
|
||
|
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
|
||
|
}
|
||
|
const correlationId = request.correlationId ||
|
||
|
this.config.cryptoInterface.createNewGuid();
|
||
|
parameterBuilder.addCorrelationId(correlationId);
|
||
|
parameterBuilder.addRequestTokenUse(AADServerParamKeys.ON_BEHALF_OF);
|
||
|
parameterBuilder.addOboAssertion(request.oboAssertion);
|
||
|
if (this.config.clientCredentials.clientSecret) {
|
||
|
parameterBuilder.addClientSecret(this.config.clientCredentials.clientSecret);
|
||
|
}
|
||
|
if (this.config.clientCredentials.clientAssertion) {
|
||
|
const clientAssertion = this.config.clientCredentials.clientAssertion;
|
||
|
parameterBuilder.addClientAssertion(clientAssertion.assertion);
|
||
|
parameterBuilder.addClientAssertionType(clientAssertion.assertionType);
|
||
|
}
|
||
|
if (request.claims ||
|
||
|
(this.config.authOptions.clientCapabilities &&
|
||
|
this.config.authOptions.clientCapabilities.length > 0)) {
|
||
|
parameterBuilder.addClaims(request.claims, this.config.authOptions.clientCapabilities);
|
||
|
}
|
||
|
return parameterBuilder.createQueryString();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { OnBehalfOfClient };
|
||
|
//# sourceMappingURL=OnBehalfOfClient.mjs.map
|