Skip to content

Commit 437fd87

Browse files
committed
harmonize towards AuthenticationType and allow disabling cache
1 parent 62d4023 commit 437fd87

File tree

6 files changed

+104
-55
lines changed

6 files changed

+104
-55
lines changed

packages/connectivity/src/scp-cf/destination/destination-accessor-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface DestinationAccessorOptions {
5151
* ATTENTION: The property is mandatory in the following cases:
5252
* - User-dependent authentication flow is used, e.g., `OAuth2UserTokenExchange`, `OAuth2JWTBearer`, `OAuth2SAMLBearerAssertion`, `SAMLAssertion` or `PrincipalPropagation`.
5353
* - Multi-tenant scenarios with destinations maintained in the subscriber account. This case is implied if the `selectionStrategy` is set to `alwaysSubscriber`.
54-
* - IAS token exchange with `iasOptions.actAs` set to `'business-user'`. In this case, the JWT is automatically used as the assertion for IAS token exchange.
54+
* - IAS token exchange with `iasOptions.authenticationType` set to `'OAuth2JWTBearer'`. In this case, the JWT is automatically used as the assertion for IAS token exchange.
5555
*/
5656
jwt?: string;
5757

packages/connectivity/src/scp-cf/destination/destination-from-vcap.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { serviceToDestinationTransformers } from './service-binding-to-destinati
1010
import { setForwardedAuthTokenIfNeeded } from './forward-auth-token';
1111
import type { Xor } from '@sap-cloud-sdk/util';
1212
import type { DestinationFetchOptions } from './destination-accessor-types';
13-
import type { Destination } from './destination-service-types';
13+
import type {
14+
AuthenticationType,
15+
Destination
16+
} from './destination-service-types';
1417
import type { CachingOptions } from '../cache';
1518
import type { Service } from '../environment-accessor';
1619
import type { JwtPayload } from '../jsonwebtoken-type';
@@ -43,7 +46,7 @@ export async function getDestinationFromServiceBinding(
4346
// If using business user authentication with IAS and no assertion provided, use the JWT from options
4447
let iasOptions = options.iasOptions;
4548
if (
46-
iasOptions?.actAs === 'business-user' &&
49+
iasOptions?.authenticationType === 'OAuth2JWTBearer' &&
4750
options.jwt &&
4851
!iasOptions.assertion
4952
) {
@@ -131,35 +134,43 @@ interface IasOptionsBase {
131134
* Additional parameters to be sent along with the token request.
132135
*/
133136
extraParams?: Record<string, string>;
137+
/**
138+
* Whether to use the cache for IAS tokens.
139+
* @default true
140+
*/
141+
useCache?: boolean;
134142
}
135143

136144
/**
137-
* IAS options for technical user authentication.
145+
* IAS options for technical user authentication (client credentials).
138146
*/
139147
type IasOptionsTechnical = IasOptionsBase & {
140-
actAs?: 'technical-user';
148+
/**
149+
* Authentication type. Use 'OAuth2ClientCredentials' for technical user (default).
150+
*/
151+
authenticationType?: Extract<AuthenticationType, 'OAuth2ClientCredentials'>;
141152
/**
142153
* Assertion not used for technical user authentication.
143154
*/
144155
assertion?: never;
145156
};
146157

147158
/**
148-
* IAS options for business user authentication.
159+
* IAS options for business user authentication (JWT bearer).
149160
*/
150161
type IasOptionsBusinessUser = IasOptionsBase & {
151162
/**
152-
* Specifies business user authentication.
163+
* Authentication type. Use 'OAuth2JWTBearer' for business user authentication.
153164
*/
154-
actAs: 'business-user';
165+
authenticationType: Extract<AuthenticationType, 'OAuth2JWTBearer'>;
155166
/**
156167
* The JWT assertion string to use for business user authentication (required).
157168
*/
158169
assertion: string;
159170
};
160171

161172
/**
162-
* Options for IAS token retrieval with type-safe actAs/jwt relationship.
173+
* Options for IAS token retrieval with type-safe authenticationType/assertion relationship.
163174
*/
164175
export type IasOptions = IasOptionsTechnical | IasOptionsBusinessUser;
165176

packages/connectivity/src/scp-cf/ias-token-cache.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('ias-token-cache', () => {
135135
});
136136

137137
const cacheKey = getCacheKey('test-client-id', {
138-
actAs: 'business-user',
138+
authenticationType: 'OAuth2JWTBearer',
139139
assertion
140140
});
141141
expect(cacheKey).toBe('user-123:tenant-456:test-client-id:');
@@ -148,7 +148,7 @@ describe('ias-token-cache', () => {
148148
});
149149

150150
const cacheKey = getCacheKey('test-client-id', {
151-
actAs: 'business-user',
151+
authenticationType: 'OAuth2JWTBearer',
152152
assertion,
153153
resource: { name: 'my-app' }
154154
});
@@ -159,11 +159,11 @@ describe('ias-token-cache', () => {
159159
jest.spyOn(logger, 'warn');
160160

161161
const cacheKey = getCacheKey('test-client-id', {
162-
actAs: 'business-user'
162+
authenticationType: 'OAuth2JWTBearer'
163163
} as any);
164164
expect(cacheKey).toBeUndefined();
165165
expect(logger.warn).toHaveBeenCalledWith(
166-
'Cannot create cache key for IAS token cache. Business-user flow requires assertion JWT.'
166+
'Cannot create cache key for IAS token cache. OAuth2JWTBearer flow requires assertion JWT.'
167167
);
168168
});
169169

@@ -175,7 +175,7 @@ describe('ias-token-cache', () => {
175175
});
176176

177177
const cacheKey = getCacheKey('test-client-id', {
178-
actAs: 'business-user',
178+
authenticationType: 'OAuth2JWTBearer',
179179
assertion
180180
});
181181
expect(cacheKey).toBeUndefined();
@@ -192,7 +192,7 @@ describe('ias-token-cache', () => {
192192
});
193193

194194
const cacheKey = getCacheKey('test-client-id', {
195-
actAs: 'business-user',
195+
authenticationType: 'OAuth2JWTBearer',
196196
assertion
197197
});
198198
expect(cacheKey).toBeUndefined();
@@ -238,11 +238,11 @@ describe('ias-token-cache', () => {
238238
});
239239

240240
const key1 = getCacheKey('test-client-id', {
241-
actAs: 'business-user',
241+
authenticationType: 'OAuth2JWTBearer',
242242
assertion: assertion1
243243
});
244244
const key2 = getCacheKey('test-client-id', {
245-
actAs: 'business-user',
245+
authenticationType: 'OAuth2JWTBearer',
246246
assertion: assertion2
247247
});
248248

packages/connectivity/src/scp-cf/ias-token-cache.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,18 @@ function normalizeResource(resource?: IasOptions['resource']): string {
7070
}
7171

7272
/**
73-
* Generates a cache key for IAS tokens based on actAs mode, user/tenant context, and resource.
73+
* Generates a cache key for IAS tokens based on authenticationType, user/tenant context, and resource.
7474
* @param clientId - The client ID from service credentials.
75-
* @param options - IAS options containing actAs, assertion, resource, and appTenantId.
75+
* @param options - IAS options containing authenticationType, assertion, resource, and appTenantId.
7676
* @returns Cache key string or undefined if required components are missing.
7777
* @internal
7878
*/
7979
export function getCacheKey(
8080
clientId: string,
8181
options: ServiceBindingTransformOptions['iasOptions'] = {}
8282
): string | undefined {
83-
const actAs = options.actAs || 'technical-user';
83+
const authenticationType =
84+
options.authenticationType || 'OAuth2ClientCredentials';
8485
const resourceStr = normalizeResource(options.resource);
8586

8687
if (!clientId) {
@@ -90,10 +91,10 @@ export function getCacheKey(
9091
return undefined;
9192
}
9293

93-
if (actAs === 'business-user') {
94+
if (authenticationType === 'OAuth2JWTBearer') {
9495
if (!options.assertion) {
9596
logger.warn(
96-
'Cannot create cache key for IAS token cache. Business-user flow requires assertion JWT.'
97+
'Cannot create cache key for IAS token cache. OAuth2JWTBearer flow requires assertion JWT.'
9798
);
9899
return undefined;
99100
}

packages/connectivity/src/scp-cf/identity-service.spec.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ describe('getIasClientCredentialsToken', () => {
269269
mockedAxios.request.mockResolvedValue({ data: mockTokenResponse });
270270

271271
await getIasClientCredentialsToken(mockIasService, {
272-
actAs: 'technical-user'
272+
authenticationType: 'OAuth2ClientCredentials'
273273
});
274274

275275
const callData = mockedAxios.request.mock.calls[0][0].data;
@@ -287,7 +287,7 @@ describe('getIasClientCredentialsToken', () => {
287287
});
288288

289289
await getIasClientCredentialsToken(mockIasService, {
290-
actAs: 'business-user',
290+
authenticationType: 'OAuth2JWTBearer',
291291
assertion: userAssertion
292292
});
293293

@@ -304,17 +304,19 @@ describe('getIasClientCredentialsToken', () => {
304304
it('throws error for business-user without assertion', async () => {
305305
await expect(
306306
getIasClientCredentialsToken(mockIasService, {
307-
actAs: 'business-user'
307+
authenticationType: 'OAuth2JWTBearer'
308308
} as any)
309-
).rejects.toThrow('JWT assertion required for actAs: "business-user"');
309+
).rejects.toThrow(
310+
'JWT assertion required for authenticationType: "OAuth2JWTBearer"'
311+
);
310312
});
311313

312314
it('includes refresh_token workaround only for business-user', async () => {
313315
mockedAxios.request.mockResolvedValue({ data: mockTokenResponse });
314316

315317
// Technical user - no refresh_token
316318
await getIasClientCredentialsToken(mockIasService, {
317-
actAs: 'technical-user'
319+
authenticationType: 'OAuth2ClientCredentials'
318320
});
319321
let callData = mockedAxios.request.mock.calls[0][0].data;
320322
expect(callData).not.toContain('refresh_token=0');
@@ -328,7 +330,7 @@ describe('getIasClientCredentialsToken', () => {
328330
app_tid: 'test-tenant'
329331
});
330332
await getIasClientCredentialsToken(mockIasService, {
331-
actAs: 'business-user',
333+
authenticationType: 'OAuth2JWTBearer',
332334
assertion: userAssertion
333335
});
334336
callData = mockedAxios.request.mock.calls[0][0].data;
@@ -344,7 +346,7 @@ describe('getIasClientCredentialsToken', () => {
344346
});
345347

346348
await getIasClientCredentialsToken(mockIasService, {
347-
actAs: 'business-user',
349+
authenticationType: 'OAuth2JWTBearer',
348350
assertion: userAssertion,
349351
resource: { name: 'my-app' },
350352
appTenantId: 'tenant-123'
@@ -390,11 +392,11 @@ describe('getIasClientCredentialsToken', () => {
390392
});
391393

392394
await getIasClientCredentialsToken(mockIasService, {
393-
actAs: 'business-user',
395+
authenticationType: 'OAuth2JWTBearer',
394396
assertion: assertion1
395397
});
396398
await getIasClientCredentialsToken(mockIasService, {
397-
actAs: 'business-user',
399+
authenticationType: 'OAuth2JWTBearer',
398400
assertion: assertion2
399401
});
400402

@@ -458,5 +460,48 @@ describe('getIasClientCredentialsToken', () => {
458460

459461
expect(mockedAxios.request).toHaveBeenCalledTimes(2);
460462
});
463+
464+
it('does not use cache when useCache is false', async () => {
465+
mockedAxios.request.mockResolvedValue({ data: mockTokenResponse });
466+
467+
const first = await getIasClientCredentialsToken(mockIasService, {
468+
useCache: false
469+
});
470+
const second = await getIasClientCredentialsToken(mockIasService, {
471+
useCache: false
472+
});
473+
474+
expect(first).toEqual(mockTokenResponse);
475+
expect(second).toEqual(mockTokenResponse);
476+
// Should make 2 network calls since caching is disabled
477+
expect(mockedAxios.request).toHaveBeenCalledTimes(2);
478+
});
479+
480+
it('does not cache token when useCache is false', async () => {
481+
mockedAxios.request.mockResolvedValue({ data: mockTokenResponse });
482+
483+
// First call with useCache=false
484+
await getIasClientCredentialsToken(mockIasService, {
485+
useCache: false
486+
});
487+
488+
// Second call with useCache=true (default) should still make a network call
489+
await getIasClientCredentialsToken(mockIasService);
490+
491+
// Should make 2 network calls: first didn't cache, second had no cached value
492+
expect(mockedAxios.request).toHaveBeenCalledTimes(2);
493+
});
494+
495+
it('uses cache by default when useCache is not specified', async () => {
496+
mockedAxios.request.mockResolvedValue({ data: mockTokenResponse });
497+
498+
const first = await getIasClientCredentialsToken(mockIasService);
499+
const second = await getIasClientCredentialsToken(mockIasService);
500+
501+
expect(first).toEqual(mockTokenResponse);
502+
expect(second).toEqual(mockTokenResponse);
503+
// Should make only 1 network call since caching is enabled by default
504+
expect(mockedAxios.request).toHaveBeenCalledTimes(1);
505+
});
461506
});
462507
});

packages/connectivity/src/scp-cf/identity-service.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,6 @@ const logger = createLogger({
2020
messageContext: 'identity-service'
2121
});
2222

23-
/**
24-
* Specifies which user identity should be used for authentication.
25-
* Determines whether to use technical client credentials or propagate a business user's identity.
26-
*/
27-
export type ActAs =
28-
/**
29-
* Technical user from the service binding (default).
30-
*/
31-
| 'technical-user'
32-
/**
33-
* Business user from the current request context (requires JWT).
34-
*/
35-
| 'business-user';
36-
3723
/**
3824
* @internal
3925
* Checks whether the IAS token to XSUAA token exchange should be applied.
@@ -57,7 +43,7 @@ type IasParameters = {
5743
* Make a client credentials request against the IAS OAuth2 endpoint.
5844
* Supports both certificate-based (mTLS) and client secret authentication.
5945
* @param service - Service as it is defined in the environment variable.
60-
* @param options - Options for token fetching, including actAs to specify authentication mode, optional resource parameter for app2app, appTenantId for multi-tenant scenarios, and extraParams for additional OAuth2 parameters.
46+
* @param options - Options for token fetching, including authenticationType to specify authentication mode, optional resource parameter for app2app, appTenantId for multi-tenant scenarios, and extraParams for additional OAuth2 parameters.
6147
* @returns Client credentials token response.
6248
* @internal
6349
*/
@@ -67,10 +53,13 @@ export async function getIasClientCredentialsToken(
6753
): Promise<ClientCredentialsResponse> {
6854
const resolvedService = resolveServiceBinding(service);
6955
const { clientid } = resolvedService.credentials;
56+
const useCache = options.useCache ?? true;
7057

71-
const cachedToken = iasTokenCache.getToken(clientid, options);
72-
if (cachedToken) {
73-
return cachedToken;
58+
if (useCache) {
59+
const cachedToken = iasTokenCache.getToken(clientid, options);
60+
if (cachedToken) {
61+
return cachedToken;
62+
}
7463
}
7564

7665
const fnArgument: IasParameters = {
@@ -95,8 +84,10 @@ export async function getIasClientCredentialsToken(
9584
);
9685
});
9786

98-
// Cache the token
99-
iasTokenCache.cacheToken(clientid, options, token);
87+
// Cache the token if caching is enabled
88+
if (useCache) {
89+
iasTokenCache.cacheToken(clientid, options, token);
90+
}
10091

10192
return token;
10293
}
@@ -124,14 +115,15 @@ async function getIasClientCredentialsTokenImpl(
124115
client_id: clientid
125116
});
126117

127-
// Determine grant type based on actAs parameter
128-
const actAs = arg.actAs || 'technical-user';
118+
// Determine grant type based on authenticationType parameter
119+
const authenticationType =
120+
arg.authenticationType || 'OAuth2ClientCredentials';
129121

130-
if (actAs === 'business-user') {
122+
if (authenticationType === 'OAuth2JWTBearer') {
131123
// JWT bearer grant for business user propagation
132124
if (!arg.assertion) {
133125
throw new Error(
134-
'JWT assertion required for actAs: "business-user". Provide iasOptions.assertion.'
126+
'JWT assertion required for authenticationType: "OAuth2JWTBearer". Provide iasOptions.assertion.'
135127
);
136128
}
137129
params.append('assertion', arg.assertion);

0 commit comments

Comments
 (0)