Skip to content

Commit d444438

Browse files
davidkna-sapcloud-sdk-jsmarikanerKavithaSiva
authored
feat: IAS App-To-App Auth (#6185)
--------- Co-authored-by: cloud-sdk-js <[email protected]> Co-authored-by: Marika Marszalkowski <[email protected]> Co-authored-by: KavithaSiva <[email protected]>
1 parent eeec5f5 commit d444438

27 files changed

+2532
-81
lines changed

.changeset/slow-cars-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-cloud-sdk/connectivity': minor
3+
---
4+
5+
[New Functionality] Support IAS (App-to-App) authentication. Use `transformServiceBindingToDestination()` function or `getDestinationFromServiceBinding()` function to create a destination targeting an IAS application.

packages/connectivity/src/http-agent/http-agent.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,35 @@ describe('getAgentConfig', () => {
358358
expect(actual.passphrase).not.toBeDefined();
359359
expect(cacheSpy).toHaveBeenCalledTimes(1);
360360
});
361+
362+
it('logs a warning when both mtls is enabled and mtlsKeyPair is provided', async () => {
363+
process.env.CF_INSTANCE_CERT = 'cf-crypto/cf-cert';
364+
process.env.CF_INSTANCE_KEY = 'cf-crypto/cf-key';
365+
366+
const destination: HttpDestination = {
367+
url: 'https://example.com',
368+
name: 'test-destination',
369+
mtls: true,
370+
mtlsKeyPair: {
371+
cert: 'ias-cert',
372+
key: 'ias-key'
373+
}
374+
};
375+
376+
const logger = createLogger('http-agent');
377+
const warnSpy = jest.spyOn(logger, 'warn');
378+
379+
const actual = (await getAgentConfig(destination))['httpsAgent']
380+
.options;
381+
382+
expect(warnSpy).toHaveBeenCalledWith(
383+
"Destination test-destination has both 'mtlsKeyPair' (used by IAS) and 'mtls' (to use certs from cf) enabled. The 'mtlsKeyPair' will be used."
384+
);
385+
expect(actual.cert).toEqual('ias-cert');
386+
expect(actual.key).toEqual('ias-key');
387+
388+
warnSpy.mockRestore();
389+
});
361390
});
362391

363392
it('returns an object with key "httpsAgent" which is missing mTLS options when mtls is set to true but env variables do not include cert & key', async () => {

packages/connectivity/src/http-agent/http-agent.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function getKeyStoreOptions(destination: Destination):
117117
if (
118118
// Only add certificates, when using ClientCertificateAuthentication (https://github.com/SAP/cloud-sdk-js/issues/3544)
119119
destination.authentication === 'ClientCertificateAuthentication' &&
120-
!mtlsIsEnabled(destination) &&
120+
!(mtlsIsEnabled(destination) || destination.mtlsKeyPair) &&
121121
destination.keyStoreName
122122
) {
123123
const certificate = selectCertificate(destination);
@@ -213,6 +213,17 @@ async function getMtlsOptions(
213213
} has mTLS enabled, but the required Cloud Foundry environment variables (CF_INSTANCE_CERT and CF_INSTANCE_KEY) are not defined. Note that 'inferMtls' only works on Cloud Foundry.`
214214
);
215215
}
216+
if (destination.mtlsKeyPair) {
217+
if (mtlsIsEnabled(destination)) {
218+
logger.warn(
219+
`Destination ${
220+
destination.name ? destination.name : ''
221+
} has both 'mtlsKeyPair' (used by IAS) and 'mtls' (to use certs from cf) enabled. The 'mtlsKeyPair' will be used.`
222+
);
223+
}
224+
225+
return destination.mtlsKeyPair;
226+
}
216227
if (mtlsIsEnabled(destination)) {
217228
if (registerDestinationCache.mtls.useMtlsCache) {
218229
return registerDestinationCache.mtls.getMtlsOptions();

packages/connectivity/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ export type {
5858
DestinationJson,
5959
DestinationsByType,
6060
DestinationFromServiceBindingOptions,
61-
ServiceBindingTransformOptions
61+
ServiceBindingTransformOptions,
62+
IasOptions,
63+
IasOptionsBase,
64+
IasOptionsBusinessUser,
65+
IasOptionsTechnicalUser,
66+
IasResource
6267
} from './scp-cf';
6368

6469
export type {

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

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { createLogger } from '@sap-cloud-sdk/util';
2-
import { clientCredentialsTokenCache } from './client-credentials-token-cache';
2+
import {
3+
clientCredentialsTokenCache,
4+
getIasCacheKey
5+
} from './client-credentials-token-cache';
36

47
const oneHourInSeconds = 60 * 60;
58

@@ -87,4 +90,217 @@ describe('ClientCredentialsTokenCache', () => {
8790
'Cannot create cache key for client credentials token cache. The given client ID is undefined.'
8891
);
8992
});
93+
94+
describe('IAS resource parameter support', () => {
95+
const validToken = {
96+
access_token: '1234567890',
97+
token_type: 'Bearer',
98+
expires_in: oneHourInSeconds * 3,
99+
jti: '',
100+
scope: ''
101+
};
102+
const iasTokenCacheData = {
103+
iasInstance: 'subscriber-tenant',
104+
clientId: 'clientid',
105+
resource: { name: 'my-app' }
106+
};
107+
108+
beforeEach(() => {
109+
clientCredentialsTokenCache.clear();
110+
});
111+
112+
it('should cache and retrieve token with resource name', () => {
113+
clientCredentialsTokenCache.cacheIasToken(iasTokenCacheData, validToken);
114+
115+
const cached = clientCredentialsTokenCache.getTokenIas(iasTokenCacheData);
116+
117+
expect(cached).toEqual(validToken);
118+
});
119+
120+
it('should cache and retrieve token with resource clientId', () => {
121+
const resource = { providerClientId: 'resource-client-123' };
122+
123+
clientCredentialsTokenCache.cacheIasToken(
124+
{
125+
...iasTokenCacheData,
126+
resource
127+
},
128+
validToken
129+
);
130+
131+
const cached = clientCredentialsTokenCache.getTokenIas({
132+
...iasTokenCacheData,
133+
resource
134+
});
135+
136+
expect(cached).toEqual(validToken);
137+
});
138+
139+
it('should cache and retrieve token with resource clientId and tenantId', () => {
140+
const resource = {
141+
providerClientId: 'resource-client-123',
142+
providerTenantId: 'tenant-456'
143+
};
144+
145+
clientCredentialsTokenCache.cacheIasToken(
146+
{
147+
...iasTokenCacheData,
148+
resource
149+
},
150+
validToken
151+
);
152+
153+
const cached = clientCredentialsTokenCache.getTokenIas({
154+
...iasTokenCacheData,
155+
resource
156+
});
157+
158+
expect(cached).toEqual(validToken);
159+
});
160+
161+
it('should isolate cache by resource name', () => {
162+
const resource1 = { name: 'app-1' };
163+
const resource2 = { name: 'app-2' };
164+
165+
clientCredentialsTokenCache.cacheIasToken(
166+
{
167+
...iasTokenCacheData,
168+
resource: resource1
169+
},
170+
validToken
171+
);
172+
173+
const cached1 = clientCredentialsTokenCache.getTokenIas({
174+
...iasTokenCacheData,
175+
resource: resource1
176+
});
177+
const cached2 = clientCredentialsTokenCache.getTokenIas({
178+
...iasTokenCacheData,
179+
resource: resource2
180+
});
181+
182+
expect(cached1).toEqual(validToken);
183+
expect(cached2).toBeUndefined();
184+
});
185+
186+
it('should isolate cache by resource providerClientId', () => {
187+
const resource1 = { providerClientId: 'client-1' };
188+
const resource2 = { providerClientId: 'client-2' };
189+
190+
clientCredentialsTokenCache.cacheIasToken(
191+
{
192+
...iasTokenCacheData,
193+
resource: resource1
194+
},
195+
validToken
196+
);
197+
198+
const cached1 = clientCredentialsTokenCache.getTokenIas({
199+
...iasTokenCacheData,
200+
resource: resource1
201+
});
202+
const cached2 = clientCredentialsTokenCache.getTokenIas({
203+
...iasTokenCacheData,
204+
resource: resource2
205+
});
206+
207+
expect(cached1).toEqual(validToken);
208+
expect(cached2).toBeUndefined();
209+
});
210+
211+
it('should generate correct cache key with resource name', () => {
212+
const key = getIasCacheKey({
213+
iasInstance: 'tenant-123',
214+
clientId: 'client-id',
215+
resource: { name: 'my-app' }
216+
});
217+
expect(key).toBe('tenant-123::client-id:name=my-app');
218+
});
219+
220+
it('should generate correct cache key with resource clientId only', () => {
221+
const key = getIasCacheKey({
222+
iasInstance: 'tenant-123',
223+
clientId: 'client-id',
224+
resource: {
225+
providerClientId: 'resource-client-123'
226+
}
227+
});
228+
expect(key).toBe(
229+
'tenant-123::client-id:provider-clientId=resource-client-123'
230+
);
231+
});
232+
233+
it('should generate correct cache key with resource clientId and tenantId', () => {
234+
const key = getIasCacheKey({
235+
iasInstance: 'tenant-123',
236+
clientId: 'client-id',
237+
resource: {
238+
providerClientId: 'resource-client-123',
239+
providerTenantId: 'tenant-456'
240+
}
241+
});
242+
expect(key).toBe(
243+
'tenant-123::client-id:provider-clientId=resource-client-123:provider-tenantId=tenant-456'
244+
);
245+
});
246+
247+
it('should generate cache key without resource when not provided', () => {
248+
const key = getIasCacheKey({
249+
iasInstance: 'tenant-123',
250+
clientId: 'client-id'
251+
});
252+
expect(key).toBe('tenant-123::client-id');
253+
});
254+
255+
it('should isolate cache by appTid', () => {
256+
clientCredentialsTokenCache.cacheIasToken(
257+
{
258+
...iasTokenCacheData,
259+
appTid: 'tenant-123'
260+
},
261+
validToken
262+
);
263+
264+
const cached1 = clientCredentialsTokenCache.getTokenIas({
265+
...iasTokenCacheData,
266+
appTid: 'tenant-123'
267+
});
268+
const cached2 = clientCredentialsTokenCache.getTokenIas({
269+
...iasTokenCacheData,
270+
appTid: 'tenant-456'
271+
});
272+
const cached3 = clientCredentialsTokenCache.getTokenIas({
273+
...iasTokenCacheData
274+
// No appTid
275+
});
276+
277+
expect(cached1).toEqual(validToken);
278+
expect(cached2).toBeUndefined();
279+
expect(cached3).toBeUndefined();
280+
});
281+
282+
it('should generate correct cache key with appTid', () => {
283+
const key = getIasCacheKey({
284+
iasInstance: 'tenant-123',
285+
clientId: 'client-id',
286+
appTid: 'app-tenant-456'
287+
});
288+
expect(key).toBe('tenant-123:app-tenant-456:client-id');
289+
});
290+
291+
it('should generate cache key with double colon when appTid is undefined', () => {
292+
const key1 = getIasCacheKey({
293+
iasInstance: 'tenant-123',
294+
clientId: 'client-id',
295+
appTid: undefined
296+
});
297+
const key2 = getIasCacheKey({
298+
iasInstance: 'tenant-123',
299+
clientId: 'client-id'
300+
});
301+
// Both should produce the same key with double colon
302+
expect(key1).toBe('tenant-123::client-id');
303+
expect(key2).toBe('tenant-123::client-id');
304+
});
305+
});
90306
});

0 commit comments

Comments
 (0)