/*! @azure/msal-node v2.5.1 2023-11-07 */ 'use strict'; 'use strict'; var msalCommon = require('@azure/msal-common'); var EncodingUtils = require('../utils/EncodingUtils.cjs'); /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * On-Behalf-Of client */ class OnBehalfOfClient extends msalCommon.BaseClient { constructor(configuration) { super(configuration); } /** * Public API to acquire tokens with on behalf of flow * @param request */ async acquireToken(request) { this.scopeSet = new msalCommon.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(msalCommon.CacheOutcome.NO_CACHED_ACCESS_TOKEN); this.logger.info("SilentFlowClient:acquireCachedToken - No access token found in cache for the given properties."); throw msalCommon.createClientAuthError(msalCommon.ClientAuthErrorCodes.tokenRefreshRequired); } else if (msalCommon.TimeUtils.isTokenExpired(cachedAccessToken.expiresOn, this.config.systemOptions.tokenRenewalOffsetSeconds)) { // Access token expired, will need to renewed this.serverTelemetryManager?.setCacheOutcome(msalCommon.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 msalCommon.createClientAuthError(msalCommon.ClientAuthErrorCodes.tokenRefreshRequired); } // fetch the idToken from cache const cachedIdToken = this.readIdTokenFromCacheForOBO(cachedAccessToken.homeAccountId); let idTokenClaims; let cachedAccount = null; if (cachedIdToken) { idTokenClaims = msalCommon.AuthToken.extractTokenClaims(cachedIdToken.secret, EncodingUtils.EncodingUtils.base64Decode); const localAccountId = idTokenClaims.oid || idTokenClaims.sub; const accountInfo = { homeAccountId: cachedIdToken.homeAccountId, environment: cachedIdToken.environment, tenantId: cachedIdToken.realm, username: msalCommon.Constants.EMPTY_STRING, localAccountId: localAccountId || msalCommon.Constants.EMPTY_STRING, }; cachedAccount = this.cacheManager.readAccountFromCache(accountInfo); } // increment telemetry cache hit counter if (this.config.serverTelemetryManager) { this.config.serverTelemetryManager.incrementCacheHits(); } return await msalCommon.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: msalCommon.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 || msalCommon.AuthenticationScheme.BEARER; /* * Distinguish between Bearer and PoP/SSH token cache types * Cast to lowercase to handle "bearer" from ADFS */ const credentialType = authScheme && authScheme.toLowerCase() !== msalCommon.AuthenticationScheme.BEARER.toLowerCase() ? msalCommon.CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME : msalCommon.CredentialType.ACCESS_TOKEN; const accessTokenFilter = { credentialType: credentialType, clientId, target: msalCommon.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 msalCommon.createClientAuthError(msalCommon.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 = msalCommon.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 = msalCommon.TimeUtils.nowSeconds(); const response = await this.executePostToTokenEndpoint(endpoint, requestBody, headers, thumbprint, request.correlationId); const responseHandler = new msalCommon.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 msalCommon.RequestParameterBuilder(); parameterBuilder.addClientId(this.config.authOptions.clientId); parameterBuilder.addScopes(request.scopes); parameterBuilder.addGrantType(msalCommon.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(msalCommon.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(); } } exports.OnBehalfOfClient = OnBehalfOfClient; //# sourceMappingURL=OnBehalfOfClient.cjs.map