diff --git a/.changeset/thick-glasses-lick.md b/.changeset/thick-glasses-lick.md new file mode 100644 index 0000000000..2f6d8e7794 --- /dev/null +++ b/.changeset/thick-glasses-lick.md @@ -0,0 +1,5 @@ +--- +'@sap-cloud-sdk/connectivity': minor +--- + +[Fixed Issue] Check `Proxy-Authorization` and `SAP-Connectivity-Authentication` tokens to determine cache expiration time. diff --git a/packages/connectivity/src/scp-cf/destination/destination-cache.spec.ts b/packages/connectivity/src/scp-cf/destination/destination-cache.spec.ts index a6c39f8bd8..7e559eb847 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-cache.spec.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-cache.spec.ts @@ -31,6 +31,7 @@ import { mockJwtBearerToken, mockServiceToken } from '../../../../../test-resources/test/test-util/token-accessor-mocks'; +import { signedJwtForVerification } from '../../../../../test-resources/test/test-util'; import { destinationServiceCache } from './destination-service-cache'; import { alwaysProvider, @@ -806,6 +807,112 @@ describe('destination cache', () => { }); }); + it('should return undefined when Proxy-Authorization token expires first', async () => { + jest.useFakeTimers(); + const twoMinutesTokenLifetime = 2 * 60; + const fourMinutesTokenLifetime = 4 * 60; + const sixMinutesTokenLifetime = 6 * 60; + const dummyJwt = { user_id: 'user', zid: 'tenant' }; + + const proxyAuthorizationToken = signedJwtForVerification({ + ...decodeJwt(providerServiceToken), + exp: twoMinutesTokenLifetime + }); + + const sapConnectivityAuthenticationToken = signedJwtForVerification({ + ...decodeJwt(providerUserToken), + exp: fourMinutesTokenLifetime + }); + + const destination = { + ...destinationOne, + authTokens: [ + { + expiresIn: sixMinutesTokenLifetime.toString() + } as DestinationAuthToken + ], + proxyConfiguration: { + ...connectivityProxyConfigMock, + headers: { + 'Proxy-Authorization': `Bearer ${proxyAuthorizationToken}`, + 'SAP-Connectivity-Authentication': `Bearer ${sapConnectivityAuthenticationToken}` + } + } + }; + + await destinationCache.cacheRetrievedDestination( + dummyJwt, + destination, + 'tenant-user' + ); + + const retrieveDestination = () => + destinationCache.retrieveDestinationFromCache( + dummyJwt, + destination.name!, + 'tenant-user' + ); + + await expect(retrieveDestination()).resolves.toEqual(destination); + + jest.advanceTimersByTime(twoMinutesTokenLifetime * 1000 + 1); + + await expect(retrieveDestination()).resolves.toBeUndefined(); + }); + + it('should return undefined when SAP-Connectivity-Authentication authorization token expires first', async () => { + jest.useFakeTimers(); + const twoMinutesTokenLifetime = 2 * 60; + const fourMinutesTokenLifetime = 4 * 60; + const sixMinutesTokenLifetime = 6 * 60; + const dummyJwt = { user_id: 'user', zid: 'tenant' }; + + const proxyAuthorizationToken = signedJwtForVerification({ + ...decodeJwt(providerServiceToken), + exp: fourMinutesTokenLifetime + }); + + const sapConnectivityAuthenticationToken = signedJwtForVerification({ + ...decodeJwt(providerUserToken), + exp: twoMinutesTokenLifetime + }); + + const destination = { + ...destinationOne, + authTokens: [ + { + expiresIn: sixMinutesTokenLifetime.toString() + } as DestinationAuthToken + ], + proxyConfiguration: { + ...connectivityProxyConfigMock, + headers: { + 'Proxy-Authorization': `Bearer ${proxyAuthorizationToken}`, + 'SAP-Connectivity-Authentication': `Bearer ${sapConnectivityAuthenticationToken}` + } + } + }; + + await destinationCache.cacheRetrievedDestination( + dummyJwt, + destination, + 'tenant-user' + ); + + const retrieveDestination = () => + destinationCache.retrieveDestinationFromCache( + dummyJwt, + destination.name!, + 'tenant-user' + ); + + await expect(retrieveDestination()).resolves.toEqual(destination); + + jest.advanceTimersByTime(twoMinutesTokenLifetime * 1000 + 1); + + await expect(retrieveDestination()).resolves.toBeUndefined(); + }); + describe('custom destination cache', () => { // Cache with expiration time const testCacheOne = new TestCache(); diff --git a/packages/connectivity/src/scp-cf/destination/destination-cache.ts b/packages/connectivity/src/scp-cf/destination/destination-cache.ts index 5a055e7b56..94d9372c9e 100644 --- a/packages/connectivity/src/scp-cf/destination/destination-cache.ts +++ b/packages/connectivity/src/scp-cf/destination/destination-cache.ts @@ -1,5 +1,5 @@ import { createLogger, first } from '@sap-cloud-sdk/util'; -import { getTenantId, userId } from '../jwt'; +import { decodeJwt, getTenantId, userId } from '../jwt'; import { AsyncCache } from '../async-cache'; import type { JwtPayload } from '../jsonwebtoken-type'; import type { AsyncCacheInterface } from '../async-cache'; @@ -217,11 +217,40 @@ async function cacheRetrievedDestination( throw new Error('The destination name is undefined.'); } - const key = getDestinationCacheKey(token, destination.name, isolation); - const expiresIn = first(destination.authTokens || [])?.expiresIn; - const expirationTime = expiresIn - ? Date.now() + parseInt(expiresIn) * 1000 + const authToken = first(destination.authTokens || []); + const proxyAuthorizationToken = + destination.proxyConfiguration?.headers?.['Proxy-Authorization']?.match( + /^Bearer (.+)/ + )?.[1]; + const sapConnectivityAuthenticationToken = + destination.proxyConfiguration?.headers?.[ + 'SAP-Connectivity-Authentication' + ]?.match(/^Bearer (.+)/)?.[1]; + + const authTokenExpiresIn = authToken?.expiresIn + ? parseInt(authToken.expiresIn) : undefined; + const proxyAuthorizationExpiresIn = proxyAuthorizationToken + ? decodeJwt(proxyAuthorizationToken).exp + : undefined; + const sapConnectivityAuthenticationExpiresIn = + sapConnectivityAuthenticationToken + ? decodeJwt(sapConnectivityAuthenticationToken).exp + : undefined; + + const expiresIn = Math.min( + ...[ + authTokenExpiresIn, + proxyAuthorizationExpiresIn, + sapConnectivityAuthenticationExpiresIn + ].filter(e => e !== undefined) + ); + const expirationTime = + expiresIn && expiresIn !== Infinity + ? Date.now() + expiresIn * 1000 + : undefined; + + const key = getDestinationCacheKey(token, destination.name, isolation); cache.set(key, { entry: destination, expires: expirationTime }); }