From 8a465b6edd42a5d297a57831270c20b60651649b Mon Sep 17 00:00:00 2001 From: Zhongpin Wang Date: Tue, 26 Aug 2025 15:44:14 +0200 Subject: [PATCH 1/5] Add forwarded auth token after caching if needed & REFACTOR --- .../destination/destination-from-env.ts | 6 +- .../destination-from-registration.ts | 6 +- .../destination/destination-from-service.ts | 182 ++++++++++-------- .../destination/destination-from-vcap.ts | 16 +- .../destination/forward-auth-token.spec.ts | 13 +- .../scp-cf/destination/forward-auth-token.ts | 13 +- 6 files changed, 129 insertions(+), 107 deletions(-) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-env.ts b/packages/connectivity/src/scp-cf/destination/destination-from-env.ts index e2a2dbafe1..d9bfb341e9 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-env.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-env.ts @@ -9,7 +9,7 @@ import { proxyStrategy } from './http-proxy-util'; import { isHttpDestination } from './destination-service-types'; -import { setForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import { addForwardedAuthTokenIfNeeded } from './forward-auth-token'; import type { Destination } from './destination-service-types'; import type { DestinationFetchOptions } from './destination-accessor-types'; @@ -103,13 +103,13 @@ export function searchEnvVariablesForDestination( if (getDestinationsEnvVariable()) { try { - const destination = getDestinationFromEnvByName(options.destinationName); + let destination = getDestinationFromEnvByName(options.destinationName); if (destination) { logger.info( `Successfully retrieved destination '${options.destinationName}' from environment variable.` ); - setForwardedAuthTokenIfNeeded(destination, options.jwt); + destination = addForwardedAuthTokenIfNeeded(destination, options.jwt); return isHttpDestination(destination) && ['internet', 'private-link'].includes(proxyStrategy(destination)) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-registration.ts b/packages/connectivity/src/scp-cf/destination/destination-from-registration.ts index 62058dad60..43d062c4fb 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-registration.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-registration.ts @@ -12,7 +12,7 @@ import { proxyStrategy } from './http-proxy-util'; import { registerDestinationCache } from './register-destination-cache'; -import { setForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import { addForwardedAuthTokenIfNeeded } from './forward-auth-token'; import type { Destination } from './destination-service-types'; import type { IsolationStrategy } from './destination-cache'; import type { DestinationFetchOptions } from './destination-accessor-types'; @@ -90,7 +90,7 @@ export type DestinationWithName = Destination & { name: string }; export async function searchRegisteredDestination( options: DestinationFetchOptions ): Promise { - const destination = + let destination = await registerDestinationCache.destination.retrieveDestinationFromCache( getJwtForCaching(options), options.destinationName, @@ -108,7 +108,7 @@ export async function searchRegisteredDestination( `Successfully retrieved destination '${options.destinationName}' from registered destinations.` ); - setForwardedAuthTokenIfNeeded(destination, options.jwt); + destination = addForwardedAuthTokenIfNeeded(destination, options.jwt); return isHttpDestination(destination) && ['internet', 'private-link'].includes(proxyStrategy(destination)) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts index 25281e9e7d..aa30a6f48f 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts @@ -33,7 +33,7 @@ import { addProxyConfigurationInternet, proxyStrategy } from './http-proxy-util'; -import { setForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import { addForwardedAuthTokenIfNeeded } from './forward-auth-token'; import type { SubscriberToken } from './get-subscriber-token'; import type { Destination } from './destination-service-types'; import type { AuthAndExchangeTokens } from './destination-service'; @@ -89,80 +89,56 @@ export class DestinationFromServiceRetriever { public static async getDestinationFromDestinationService( options: DestinationFetchOptions ): Promise { - // TODO: This is currently always skipped for tokens issued by XSUAA - // in the XSUAA case no exchange takes place - if (shouldExchangeToken(options) && options.jwt) { - // Exchange the IAS token to a XSUAA token using the destination service credentials - options.jwt = await jwtBearerToken(options.jwt, 'destination'); + // Exchange the IAS token to a XSUAA token using the destination service credentials + if (shouldExchangeToken(options)) { + options.jwt = await jwtBearerToken(options.jwt!, 'destination'); } - const subscriberToken = await getSubscriberToken(options); - const providerToken = await getProviderServiceToken(options); - - const da = new DestinationFromServiceRetriever( + // Create retriever with subscriber and provider tokens + const retriever = new DestinationFromServiceRetriever( options, - subscriberToken, - providerToken + await getSubscriberToken(options), + await getProviderServiceToken(options) ); - const destinationResult = - await da.searchDestinationWithSelectionStrategyAndCache(); - if (!destinationResult) { + // Search destination with selection strategy and cache + const destinationSearchResult = await retriever.searchDestinationWithSelectionStrategyAndCache(); + + // Immediately return null if no destination found + if (!destinationSearchResult) { return null; } - let { destination } = destinationResult; - - setForwardedAuthTokenIfNeeded(destination, options.jwt); + const { origin, fromCache } = destinationSearchResult; + let { destination } = destinationSearchResult; - if (destinationResult.fromCache) { - return da.addProxyConfiguration(destination); - } - - if (!destination.forwardAuthToken) { - if ( - destination.authentication === 'OAuth2UserTokenExchange' || - destination.authentication === 'OAuth2JWTBearer' || - destination.authentication === 'SAMLAssertion' || - (destination.authentication === 'OAuth2SAMLBearerAssertion' && - !da.usesSystemUser(destination)) - ) { - destination = - await da.fetchDestinationWithUserExchangeFlows(destinationResult); - } + if (!fromCache) { + /* Destination NOT from cache */ - if (destination.authentication === 'PrincipalPropagation') { - if (!this.isUserJwt(da.subscriberToken)) { - DestinationFromServiceRetriever.throwUserTokenMissing(destination); - } - } + // Fetch and add auth token if needed, + // meaning `forwardAuthToken` is `false` + // AND authentication is one of the supported types + destination = await retriever.fetchAndAddAuthTokenIfNeeded(destination, origin); - if ( - destination.authentication === 'OAuth2Password' || - destination.authentication === 'ClientCertificateAuthentication' || - destination.authentication === 'OAuth2ClientCredentials' || - da.usesSystemUser(destination) - ) { - destination = - await da.fetchDestinationWithNonUserExchangeFlows(destinationResult); - } + // Add trust store configuration if needed, + // meaning `TrustStoreLocation` is defined + destination = await retriever.addTrustStoreConfigurationIfNeeded(destination, origin); - if (destination.authentication === 'OAuth2RefreshToken') { - destination = - await da.fetchDestinationWithRefreshTokenFlow(destinationResult); - } + // Cache the destination + await retriever.cacheDestination(destination, origin); } - const withTrustStore = await da.addTrustStoreConfiguration( - destination, - destinationResult.origin - ); - await da.updateDestinationCache(withTrustStore, destinationResult.origin); + // Add auth token based on the given `options.jwt` if needed + // meaning `forwardAuthToken` is `true` + destination = addForwardedAuthTokenIfNeeded(destination, options.jwt); + + // Add proxy configuration based on the proxy strategy + destination = await retriever.addProxyConfiguration(destination); - return da.addProxyConfiguration(withTrustStore); + return destination; } - private static throwUserTokenMissing(destination) { + private static throwUserTokenMissing(destination: Destination) { throw Error( `No user token (JWT) has been provided. This is strictly necessary for '${destination.authentication}'.` ); @@ -247,9 +223,9 @@ export class DestinationFromServiceRetriever { } private async getAuthTokenForOAuth2ClientCredentials( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { - const { destination, origin } = destinationResult; // This covers the x-tenant case https://api.sap.com/api/SAP_CP_CF_Connectivity_Destination/resource const exchangeTenant = this.getExchangeTenant(destination); const authHeaderJwt = @@ -285,9 +261,9 @@ Possible alternatives for such technical user authentication are BasicAuthentica } private async getAuthTokenForOAuth2UserBasedTokenExchanges( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { - const { destination, origin } = destinationResult; const { destinationName } = this.options; if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { throw DestinationFromServiceRetriever.throwUserTokenMissing(destination); @@ -328,7 +304,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica origin === 'provider' ? this.providerServiceToken : // on type level this could be undefined, but logically if the origin is subscriber, it must be defined. - this.subscriberToken.serviceJwt!; + this.subscriberToken.serviceJwt!; logger.debug( `UserExchange flow started for destination ${destinationName} of the ${origin} account.` @@ -341,9 +317,9 @@ Possible alternatives for such technical user authentication are BasicAuthentica } private async getAuthTokenForOAuth2RefreshToken( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { - const { destination, origin } = destinationResult; const { refreshToken } = this.options; if (!refreshToken) { throw Error( @@ -361,14 +337,16 @@ Possible alternatives for such technical user authentication are BasicAuthentica * @internal * This method calls the 'find destination by name' endpoint of the destination service using a client credentials grant. * For the find by name endpoint, the destination service will take care of OAuth flows and include the token in the destination. - * @param destinationResult - Result of the getDestinations call for which the exchange flow is triggered. + * @param destination - The destination for which the token should be fetched. + * @param origin - The origin of the destination, either 'subscriber' or 'provider'. * @returns Destination containing the auth token. */ private async fetchDestinationWithNonUserExchangeFlows( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { const token = - await this.getAuthTokenForOAuth2ClientCredentials(destinationResult); + await this.getAuthTokenForOAuth2ClientCredentials(destination, origin); return fetchDestinationWithTokenRetrieval( getDestinationServiceCredentials().uri, @@ -378,11 +356,13 @@ Possible alternatives for such technical user authentication are BasicAuthentica } private async fetchDestinationWithUserExchangeFlows( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { const token = await this.getAuthTokenForOAuth2UserBasedTokenExchanges( - destinationResult + destination, + origin ); return fetchDestinationWithTokenRetrieval( @@ -393,10 +373,11 @@ Possible alternatives for such technical user authentication are BasicAuthentica } private async fetchDestinationWithRefreshTokenFlow( - destinationResult: DestinationSearchResult + destination: Destination, + origin: DestinationOrigin ): Promise { const token = - await this.getAuthTokenForOAuth2RefreshToken(destinationResult); + await this.getAuthTokenForOAuth2RefreshToken(destination, origin); return fetchDestinationWithTokenRetrieval( getDestinationServiceCredentials().uri, @@ -405,6 +386,41 @@ Possible alternatives for such technical user authentication are BasicAuthentica ); } + private async fetchAndAddAuthTokenIfNeeded( + destination: Destination, + origin: DestinationOrigin + ): Promise { + const { forwardAuthToken, authentication } = destination; + if (forwardAuthToken) { + return destination; + } + + if ( + authentication === 'OAuth2UserTokenExchange' || + authentication === 'OAuth2JWTBearer' || + authentication === 'SAMLAssertion' || + (authentication === 'OAuth2SAMLBearerAssertion' && + !this.usesSystemUser(destination)) + ) { + destination = await this.fetchDestinationWithUserExchangeFlows(destination, origin); + } else if (authentication === 'PrincipalPropagation') { + if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { + DestinationFromServiceRetriever.throwUserTokenMissing(destination); + } + } else if ( + authentication === 'OAuth2Password' || + authentication === 'ClientCertificateAuthentication' || + authentication === 'OAuth2ClientCredentials' || + this.usesSystemUser(destination) + ) { + destination = await this.fetchDestinationWithNonUserExchangeFlows(destination, origin); + } else if (authentication === 'OAuth2RefreshToken') { + destination = + await this.fetchDestinationWithRefreshTokenFlow(destination, origin); + } + return destination; + } + private async addProxyConfiguration( destination: Destination ): Promise { @@ -429,12 +445,12 @@ Possible alternatives for such technical user authentication are BasicAuthentica } } - private async updateDestinationCache( + private async cacheDestination( destination: Destination, destinationOrigin: DestinationOrigin - ) { + ): Promise { if (!this.options.useCache) { - return destination; + return; } await destinationCache.cacheRetrievedDestination( destinationOrigin === 'subscriber' @@ -534,7 +550,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica if ( this.options.selectionStrategy.toString() === - subscriberFirst.toString() && + subscriberFirst.toString() && resultFromSubscriber ) { return false; @@ -571,20 +587,24 @@ Possible alternatives for such technical user authentication are BasicAuthentica ); } - private async addTrustStoreConfiguration( + private async addTrustStoreConfigurationIfNeeded( destination: Destination, origin: DestinationOrigin ): Promise { - if (destination.originalProperties?.TrustStoreLocation) { + const trustStoreLocation = destination.originalProperties?.TrustStoreLocation; + if (trustStoreLocation) { const trustStoreCertificate = await fetchCertificate( getDestinationServiceCredentials().uri, origin === 'provider' ? this.providerServiceToken.encoded : this.subscriberToken!.serviceJwt!.encoded, - destination.originalProperties.TrustStoreLocation + trustStoreLocation ); - destination.trustStoreCertificate = trustStoreCertificate; - } + return { + ...destination, + trustStoreCertificate + }; + }; return destination; } } diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts b/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts index 6549608b59..d50332845e 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts @@ -7,7 +7,7 @@ import { } from './http-proxy-util'; import { isHttpDestination } from './destination-service-types'; import { serviceToDestinationTransformers } from './service-binding-to-destination'; -import { setForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import { addForwardedAuthTokenIfNeeded } from './forward-auth-token'; import type { DestinationFetchOptions } from './destination-accessor-types'; import type { Destination } from './destination-service-types'; import type { CachingOptions } from '../cache'; @@ -40,20 +40,14 @@ export async function getDestinationFromServiceBinding( : undefined; const retrievalOptions = { ...options, jwt: decodedJwt }; - const destination = await retrieveDestination(retrievalOptions); + let destination = await retrieveDestination(retrievalOptions) as Destination; - const destWithProxy = - destination && - isHttpDestination(destination) && + destination = addForwardedAuthTokenIfNeeded(destination, options.jwt); + + return isHttpDestination(destination) && ['internet', 'private-link'].includes(proxyStrategy(destination)) ? addProxyConfigurationInternet(destination) : destination; - - if (destWithProxy) { - setForwardedAuthTokenIfNeeded(destWithProxy, options.jwt); - } - - return destWithProxy; } async function retrieveDestination({ diff --git a/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts b/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts index 9dd8c23677..14564e3a26 100644 --- a/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts +++ b/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts @@ -1,12 +1,14 @@ import { createLogger } from '@sap-cloud-sdk/util'; import { signedJwt } from '../../../../../test-resources/test/test-util'; -import { setForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import { addForwardedAuthTokenIfNeeded } from './forward-auth-token'; +import type { Destination } from './destination-service-types'; describe('forward auth token', () => { it('sets an auth token, when `forwardAuthToken` is set', () => { const jwt = signedJwt({}); - const destination = setForwardedAuthTokenIfNeeded( - { forwardAuthToken: true }, + let destination: Destination = { forwardAuthToken: true }; + destination = addForwardedAuthTokenIfNeeded( + destination, jwt ); expect(destination.authTokens?.[0]).toMatchObject({ value: jwt }); @@ -14,7 +16,8 @@ describe('forward auth token', () => { it('does not set an auth token, when `forwardAuthToken` is not set', () => { const jwt = signedJwt({}); - const destination = setForwardedAuthTokenIfNeeded({}, jwt); + let destination: Destination = {}; + destination = addForwardedAuthTokenIfNeeded({}, jwt); expect(destination.authTokens).toBeUndefined(); }); @@ -24,7 +27,7 @@ describe('forward auth token', () => { }); const warnSpy = jest.spyOn(logger, 'warn'); - setForwardedAuthTokenIfNeeded({ forwardAuthToken: true }); + addForwardedAuthTokenIfNeeded({ forwardAuthToken: true }); expect(warnSpy).toHaveBeenCalledWith( "Option 'forwardAuthToken' was set on destination but no token was provided to forward. This is most likely unintended and will lead to an authorization error on request execution." ); diff --git a/packages/connectivity/src/scp-cf/destination/forward-auth-token.ts b/packages/connectivity/src/scp-cf/destination/forward-auth-token.ts index c6e1920772..234acfd1dc 100644 --- a/packages/connectivity/src/scp-cf/destination/forward-auth-token.ts +++ b/packages/connectivity/src/scp-cf/destination/forward-auth-token.ts @@ -42,21 +42,26 @@ function validateToken(token: string | undefined): token is string { /** * @internal - * Set forwarded auth token, if needed. - * @param destination - Destination to set the token on, if needed. + * Add forwarded auth token, if needed. + * @param destination - Destination to add the token on, if needed. * @param token - Token to forward, if needed. */ -export function setForwardedAuthTokenIfNeeded( +export function addForwardedAuthTokenIfNeeded( destination: Destination, token?: string ): Destination { if (destination.forwardAuthToken) { + let authTokens: [DestinationAuthToken] | undefined; if (validateToken(token)) { logger.debug( "Option 'forwardAuthToken' enabled on destination. Using the given token for the destination." ); - destination.authTokens = buildDestinationAuthToken(token); + authTokens = buildDestinationAuthToken(token); } + return { + ...destination, + authTokens + }; } return destination; } From c92bcaa9319934cfd63dcbb3e56830474dd6a7bb Mon Sep 17 00:00:00 2001 From: Zhongpin Wang Date: Tue, 26 Aug 2025 16:17:27 +0200 Subject: [PATCH 2/5] lint --- .../destination/destination-from-service.ts | 71 ++++++++++++------- .../destination/destination-from-vcap.ts | 8 ++- .../destination/forward-auth-token.spec.ts | 5 +- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts index aa30a6f48f..3296de1999 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts @@ -102,7 +102,8 @@ export class DestinationFromServiceRetriever { ); // Search destination with selection strategy and cache - const destinationSearchResult = await retriever.searchDestinationWithSelectionStrategyAndCache(); + const destinationSearchResult = + await retriever.searchDestinationWithSelectionStrategyAndCache(); // Immediately return null if no destination found if (!destinationSearchResult) { @@ -118,11 +119,21 @@ export class DestinationFromServiceRetriever { // Fetch and add auth token if needed, // meaning `forwardAuthToken` is `false` // AND authentication is one of the supported types - destination = await retriever.fetchAndAddAuthTokenIfNeeded(destination, origin); + + // TODO: Check if the auth token is bound to options.jwt which might change next time for caching. + + // + destination = await retriever.fetchAndAddAuthTokenIfNeeded( + destination, + origin + ); // Add trust store configuration if needed, // meaning `TrustStoreLocation` is defined - destination = await retriever.addTrustStoreConfigurationIfNeeded(destination, origin); + destination = await retriever.addTrustStoreConfigurationIfNeeded( + destination, + origin + ); // Cache the destination await retriever.cacheDestination(destination, origin); @@ -304,7 +315,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica origin === 'provider' ? this.providerServiceToken : // on type level this could be undefined, but logically if the origin is subscriber, it must be defined. - this.subscriberToken.serviceJwt!; + this.subscriberToken.serviceJwt!; logger.debug( `UserExchange flow started for destination ${destinationName} of the ${origin} account.` @@ -345,8 +356,10 @@ Possible alternatives for such technical user authentication are BasicAuthentica destination: Destination, origin: DestinationOrigin ): Promise { - const token = - await this.getAuthTokenForOAuth2ClientCredentials(destination, origin); + const token = await this.getAuthTokenForOAuth2ClientCredentials( + destination, + origin + ); return fetchDestinationWithTokenRetrieval( getDestinationServiceCredentials().uri, @@ -359,11 +372,10 @@ Possible alternatives for such technical user authentication are BasicAuthentica destination: Destination, origin: DestinationOrigin ): Promise { - const token = - await this.getAuthTokenForOAuth2UserBasedTokenExchanges( - destination, - origin - ); + const token = await this.getAuthTokenForOAuth2UserBasedTokenExchanges( + destination, + origin + ); return fetchDestinationWithTokenRetrieval( getDestinationServiceCredentials().uri, @@ -376,8 +388,10 @@ Possible alternatives for such technical user authentication are BasicAuthentica destination: Destination, origin: DestinationOrigin ): Promise { - const token = - await this.getAuthTokenForOAuth2RefreshToken(destination, origin); + const token = await this.getAuthTokenForOAuth2RefreshToken( + destination, + origin + ); return fetchDestinationWithTokenRetrieval( getDestinationServiceCredentials().uri, @@ -402,21 +416,29 @@ Possible alternatives for such technical user authentication are BasicAuthentica (authentication === 'OAuth2SAMLBearerAssertion' && !this.usesSystemUser(destination)) ) { - destination = await this.fetchDestinationWithUserExchangeFlows(destination, origin); - } else if (authentication === 'PrincipalPropagation') { - if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { - DestinationFromServiceRetriever.throwUserTokenMissing(destination); - } + destination = await this.fetchDestinationWithUserExchangeFlows( + destination, + origin + ); } else if ( authentication === 'OAuth2Password' || authentication === 'ClientCertificateAuthentication' || authentication === 'OAuth2ClientCredentials' || this.usesSystemUser(destination) ) { - destination = await this.fetchDestinationWithNonUserExchangeFlows(destination, origin); + destination = await this.fetchDestinationWithNonUserExchangeFlows( + destination, + origin + ); } else if (authentication === 'OAuth2RefreshToken') { - destination = - await this.fetchDestinationWithRefreshTokenFlow(destination, origin); + destination = await this.fetchDestinationWithRefreshTokenFlow( + destination, + origin + ); + } else if (authentication === 'PrincipalPropagation') { + if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { + DestinationFromServiceRetriever.throwUserTokenMissing(destination); + } } return destination; } @@ -550,7 +572,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica if ( this.options.selectionStrategy.toString() === - subscriberFirst.toString() && + subscriberFirst.toString() && resultFromSubscriber ) { return false; @@ -591,7 +613,8 @@ Possible alternatives for such technical user authentication are BasicAuthentica destination: Destination, origin: DestinationOrigin ): Promise { - const trustStoreLocation = destination.originalProperties?.TrustStoreLocation; + const trustStoreLocation = + destination.originalProperties?.TrustStoreLocation; if (trustStoreLocation) { const trustStoreCertificate = await fetchCertificate( getDestinationServiceCredentials().uri, @@ -604,7 +627,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica ...destination, trustStoreCertificate }; - }; + } return destination; } } diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts b/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts index d50332845e..3dc305f37c 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts @@ -40,14 +40,16 @@ export async function getDestinationFromServiceBinding( : undefined; const retrievalOptions = { ...options, jwt: decodedJwt }; - let destination = await retrieveDestination(retrievalOptions) as Destination; + let destination = (await retrieveDestination( + retrievalOptions + )) as Destination; destination = addForwardedAuthTokenIfNeeded(destination, options.jwt); return isHttpDestination(destination) && ['internet', 'private-link'].includes(proxyStrategy(destination)) - ? addProxyConfigurationInternet(destination) - : destination; + ? addProxyConfigurationInternet(destination) + : destination; } async function retrieveDestination({ diff --git a/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts b/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts index 14564e3a26..7458c9be72 100644 --- a/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts +++ b/packages/connectivity/src/scp-cf/destination/forward-auth-token.spec.ts @@ -7,10 +7,7 @@ describe('forward auth token', () => { it('sets an auth token, when `forwardAuthToken` is set', () => { const jwt = signedJwt({}); let destination: Destination = { forwardAuthToken: true }; - destination = addForwardedAuthTokenIfNeeded( - destination, - jwt - ); + destination = addForwardedAuthTokenIfNeeded(destination, jwt); expect(destination.authTokens?.[0]).toMatchObject({ value: jwt }); }); From 767349f3d30852f0a3fb3376dee038e5db88a434 Mon Sep 17 00:00:00 2001 From: Zhongpin Wang Date: Tue, 26 Aug 2025 16:46:39 +0200 Subject: [PATCH 3/5] Add RCA --- .../destination/destination-from-service.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts index 3296de1999..9914c4ae95 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts @@ -121,8 +121,6 @@ export class DestinationFromServiceRetriever { // AND authentication is one of the supported types // TODO: Check if the auth token is bound to options.jwt which might change next time for caching. - - // destination = await retriever.fetchAndAddAuthTokenIfNeeded( destination, origin @@ -416,6 +414,10 @@ Possible alternatives for such technical user authentication are BasicAuthentica (authentication === 'OAuth2SAMLBearerAssertion' && !this.usesSystemUser(destination)) ) { + // VERY BAD... + // If origin is provider, next time subscriber jwt might change. + // -> It might be an invalid user jwt next time, and SDK won't throw as destination cached already. + // -> SDK will use auth token retrieved with the previous subscriber jwt for a different subscriber next time. destination = await this.fetchDestinationWithUserExchangeFlows( destination, origin @@ -426,21 +428,37 @@ Possible alternatives for such technical user authentication are BasicAuthentica authentication === 'OAuth2ClientCredentials' || this.usesSystemUser(destination) ) { + // OK! + // If origin is provider + // -> Auth token can be cached in destination cache as subscriber jwt is not used. + // If origin is subscriber + // -> Auth token can be cached in destination cache as destination is tenant-isolated. destination = await this.fetchDestinationWithNonUserExchangeFlows( destination, origin ); } else if (authentication === 'OAuth2RefreshToken') { + // OK! + // If origin is provider, provider jwt + refresh token is used. + // -> Auth token can be cached in destination cache as subscriber is not used. + // If origin is subscriber, subscriber jwt + refresh token is used. + // -> Auth token can be cached in destination cache as destination is tenant-isolated. destination = await this.fetchDestinationWithRefreshTokenFlow( destination, origin ); } else if (authentication === 'PrincipalPropagation') { + // BAD... + // If origin is provider, next time subscriber jwt might change + // -> It might be an invalid user jwt next time, and SDK won't throw as destination cached already. if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { DestinationFromServiceRetriever.throwUserTokenMissing(destination); } } return destination; + + // For BAD cases above, we need to isolate the cache additionally with the subscriber jwt (better than using user jwt directly). + // `getSubscriberToken()` can be called freely as it is calling `serviceToken()` which uses caching itself. } private async addProxyConfiguration( From 7d9dd6cbacbe12bc0ba98b074c808a490d21f3d5 Mon Sep 17 00:00:00 2001 From: Zhongpin Wang Date: Fri, 29 Aug 2025 09:49:05 +0200 Subject: [PATCH 4/5] chore: improve RCA --- .../src/scp-cf/destination/destination-from-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts index 9914c4ae95..95d45d5da0 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts @@ -428,9 +428,11 @@ Possible alternatives for such technical user authentication are BasicAuthentica authentication === 'OAuth2ClientCredentials' || this.usesSystemUser(destination) ) { - // OK! + // SOMETIMES BAD! // If origin is provider // -> Auth token can be cached in destination cache as subscriber jwt is not used. + // -> UNLESS for `OAuth2ClientCredentials`, if `x-tenant` is used with `tokenServiceURLType` set to `Common` (see `getExchangeTenant()`), + // then again tenant id is sent to destination service and jwt will be exchanged based on the templated token service url. // If origin is subscriber // -> Auth token can be cached in destination cache as destination is tenant-isolated. destination = await this.fetchDestinationWithNonUserExchangeFlows( From 140bbc3b23cbbd52e2f3a7a8bcac12379b9af58f Mon Sep 17 00:00:00 2001 From: Zhongpin Wang Date: Fri, 29 Aug 2025 10:09:45 +0200 Subject: [PATCH 5/5] Improve RCA --- .../src/scp-cf/destination/destination-from-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts index 95d45d5da0..a31019558d 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-from-service.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-from-service.ts @@ -432,7 +432,7 @@ Possible alternatives for such technical user authentication are BasicAuthentica // If origin is provider // -> Auth token can be cached in destination cache as subscriber jwt is not used. // -> UNLESS for `OAuth2ClientCredentials`, if `x-tenant` is used with `tokenServiceURLType` set to `Common` (see `getExchangeTenant()`), - // then again tenant id is sent to destination service and jwt will be exchanged based on the templated token service url. + // then the subdomain of the tenant is sent to destination service and jwt will be exchanged based on the templated token service url. // If origin is subscriber // -> Auth token can be cached in destination cache as destination is tenant-isolated. destination = await this.fetchDestinationWithNonUserExchangeFlows(