Skip to content

Commit fd1fd10

Browse files
authored
Merge branch 'main' into main
2 parents 5f32616 + f0be91a commit fd1fd10

File tree

9 files changed

+160
-18
lines changed

9 files changed

+160
-18
lines changed

.github/ISSUE_TEMPLATE/bug-report.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ body:
4444
- Java and Maven version via `mvn --version`: ...
4545
- SAP Cloud SDK version: ...
4646
- Spring Boot or CAP version: ...
47-
- <details><summary>Dependency tree via `mvn dependency:tree`</summary>
47+
48+
<details><summary>Dependency tree via <code>mvn dependency:tree</code></summary>
4849
49-
```
50-
Dependency tree here
51-
```
52-
</details>
50+
```
51+
Dependency tree here
52+
```
53+
</details>
5354
validations:
5455
required: true
5556
- type: textarea

cloudplatform/connectivity-oauth/pom.xml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@
126126
<groupId>org.apache.commons</groupId>
127127
<artifactId>commons-lang3</artifactId>
128128
</dependency>
129+
<dependency>
130+
<groupId>com.google.guava</groupId>
131+
<artifactId>guava</artifactId>
132+
</dependency>
129133
<!-- scope "provided" -->
130134
<dependency>
131135
<groupId>org.projectlombok</groupId>
@@ -184,11 +188,6 @@
184188
<artifactId>testutil</artifactId>
185189
<scope>test</scope>
186190
</dependency>
187-
<dependency>
188-
<groupId>com.google.guava</groupId>
189-
<artifactId>guava</artifactId>
190-
<scope>test</scope>
191-
</dependency>
192191
</dependencies>
193192
<build>
194193
<plugins>

cloudplatform/connectivity-oauth/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultOAuth2PropertySupplier.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public OAuth2Options getOAuth2Options()
134134
{
135135
final OAuth2Options.Builder builder = OAuth2Options.builder();
136136
options.getOption(OAuth2Options.TokenRetrievalTimeout.class).peek(builder::withTimeLimiter);
137+
options.getOption(OAuth2Options.TokenCacheParameters.class).peek(builder::withTokenCacheParameters);
137138
return builder.build();
138139
}
139140

cloudplatform/connectivity-oauth/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/OAuth2Options.java

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import javax.annotation.Nonnull;
99
import javax.annotation.Nullable;
1010

11+
import com.google.common.annotations.Beta;
1112
import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions.OptionsEnhancer;
1213
import com.sap.cloud.sdk.cloudplatform.resilience.ResilienceConfiguration.TimeLimiterConfiguration;
1314

@@ -33,11 +34,23 @@ public final class OAuth2Options
3334
* @since 5.12.0
3435
*/
3536
public static final TimeLimiterConfiguration DEFAULT_TIMEOUT = TimeLimiterConfiguration.of(Duration.ofSeconds(10));
37+
38+
/**
39+
* Default token cache configuration used by {@link OAuth2Service}. Effective defaults: 1 hour duration, 1000
40+
* entries, 30 seconds delta and cache statistics disabled.
41+
*
42+
* @see com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration#DEFAULT
43+
* @since 5.21.0
44+
*/
45+
public static final TokenCacheParameters DEFAULT_TOKEN_CACHE_PARAMETERS =
46+
TokenCacheParameters.of(Duration.ofHours(1), 1000, Duration.ofSeconds(30));
47+
3648
/**
3749
* The default {@link OAuth2Options} instance that does not alter the token retrieval process and does not use mTLS
3850
* for the target system connection.
3951
*/
40-
public static final OAuth2Options DEFAULT = new OAuth2Options(false, Map.of(), DEFAULT_TIMEOUT, null);
52+
public static final OAuth2Options DEFAULT =
53+
new OAuth2Options(false, Map.of(), DEFAULT_TIMEOUT, null, DEFAULT_TOKEN_CACHE_PARAMETERS);
4154

4255
private final boolean skipTokenRetrieval;
4356
@Nonnull
@@ -58,6 +71,15 @@ public final class OAuth2Options
5871
@Getter
5972
private final KeyStore clientKeyStore;
6073

74+
/**
75+
* Configuration for caching OAuth2 tokens.
76+
*
77+
* @since 5.21.0
78+
*/
79+
@Nonnull
80+
@Getter
81+
private final TokenCacheParameters tokenCacheParameters;
82+
6183
/**
6284
* Indicates whether to skip the OAuth2 token flow.
6385
*
@@ -101,6 +123,7 @@ public static class Builder
101123
private final Map<String, String> additionalTokenRetrievalParameters = new HashMap<>();
102124
private KeyStore clientKeyStore;
103125
private TimeLimiterConfiguration timeLimiter = DEFAULT_TIMEOUT;
126+
private TokenCacheParameters tokenCacheParameters = DEFAULT_TOKEN_CACHE_PARAMETERS;
104127

105128
/**
106129
* Indicates whether to skip the OAuth2 token flow.
@@ -178,6 +201,21 @@ public Builder withTimeLimiter( @Nonnull final TimeLimiterConfiguration timeLimi
178201
return this;
179202
}
180203

204+
/**
205+
* Set a custom token cache configuration. {@link #DEFAULT_TOKEN_CACHE_PARAMETERS} by default.
206+
*
207+
* @param tokenCacheParameters
208+
* The custom token cache parameters.
209+
* @return This {@link Builder}.
210+
* @since 5.21.0
211+
*/
212+
@Nonnull
213+
public Builder withTokenCacheParameters( @Nonnull final TokenCacheParameters tokenCacheParameters )
214+
{
215+
this.tokenCacheParameters = tokenCacheParameters;
216+
return this;
217+
}
218+
181219
/**
182220
* Creates a new {@link OAuth2Options} instance.
183221
*
@@ -198,7 +236,8 @@ public OAuth2Options build()
198236
skipTokenRetrieval,
199237
new HashMap<>(additionalTokenRetrievalParameters),
200238
timeLimiter,
201-
clientKeyStore);
239+
clientKeyStore,
240+
tokenCacheParameters);
202241
}
203242
}
204243

@@ -214,4 +253,51 @@ public static class TokenRetrievalTimeout implements OptionsEnhancer<TimeLimiter
214253
@Nonnull
215254
private final TimeLimiterConfiguration value;
216255
}
256+
257+
/**
258+
* Configuration for the token <em>response</em> cache used by {@link OAuth2Service}.
259+
*
260+
* <p>
261+
* <strong>Important:</strong> These values are passed to
262+
* {@link com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration} used by XSUAAs
263+
* {@code DefaultOAuth2TokenService}. This cache stores the HTTP token response (including the token) and it governs
264+
* the cache entry, <em>not</em> the token's lifetime.
265+
*
266+
* <p>
267+
* Expired (or almost expired) tokens are never served, regardless of {@link #cacheDuration} as xsuaa checks
268+
* <code>exp - {@link #tokenExpirationDelta}</code> before returning a cached entry.
269+
*
270+
* @since 5.21.0
271+
*/
272+
@Beta
273+
@Getter
274+
@RequiredArgsConstructor( staticName = "of" )
275+
public static class TokenCacheParameters implements OptionsEnhancer<TokenCacheParameters>
276+
{
277+
/**
278+
* Upper bound for how long a successful token response may remain cached. A cached entry is ignored earlier if
279+
* the token would be (almost) expired.
280+
*/
281+
@Nonnull
282+
private final Duration cacheDuration;
283+
/**
284+
* The maximum number of tokens to cache.
285+
*/
286+
@Nonnull
287+
private final Integer cacheSize;
288+
/**
289+
* The delta to be subtracted from the token expiration time to determine how early should a token be refreshed
290+
* before it expires.
291+
*/
292+
@Nonnull
293+
private final Duration tokenExpirationDelta;
294+
295+
@Override
296+
@Nonnull
297+
public TokenCacheParameters getValue()
298+
{
299+
return this;
300+
}
301+
}
302+
217303
}

cloudplatform/connectivity-oauth/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/OAuth2Service.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
1919
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
2020
import com.sap.cloud.sdk.cloudplatform.cache.CacheManager;
21+
import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2Options.TokenCacheParameters;
2122
import com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.ZtisClientIdentity;
2223
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
2324
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationOAuthTokenException;
@@ -39,6 +40,7 @@
3940
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
4041
import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
4142
import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
43+
import com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration;
4244

4345
import io.vavr.CheckedFunction0;
4446
import io.vavr.control.Try;
@@ -89,6 +91,8 @@ class OAuth2Service
8991
@Nonnull
9092
@Getter( AccessLevel.PACKAGE )
9193
private final ResilienceConfiguration resilienceConfiguration;
94+
@Nonnull
95+
private final TokenCacheParameters tokenCacheParameters;
9296

9397
// package-private for testing
9498
@Nonnull
@@ -101,8 +105,16 @@ OAuth2TokenService getTokenService( @Nullable final String tenantId )
101105
@Nonnull
102106
private OAuth2TokenService createTokenService( @Nonnull final CacheKey ignored )
103107
{
108+
final var tokenCacheConfiguration =
109+
TokenCacheConfiguration
110+
.getInstance(
111+
tokenCacheParameters.getCacheDuration(),
112+
tokenCacheParameters.getCacheSize(),
113+
tokenCacheParameters.getTokenExpirationDelta(),
114+
false); // disable cache statistics
115+
104116
if( !(identity instanceof ZtisClientIdentity) ) {
105-
return new DefaultOAuth2TokenService(HttpClientFactory.create(identity));
117+
return new DefaultOAuth2TokenService(HttpClientFactory.create(identity), tokenCacheConfiguration);
106118
}
107119

108120
final DefaultHttpDestination destination =
@@ -115,7 +127,9 @@ private OAuth2TokenService createTokenService( @Nonnull final CacheKey ignored )
115127
.keyStore(((ZtisClientIdentity) identity).getKeyStore())
116128
.build();
117129
try {
118-
return new DefaultOAuth2TokenService((CloseableHttpClient) HttpClientAccessor.getHttpClient(destination));
130+
return new DefaultOAuth2TokenService(
131+
(CloseableHttpClient) HttpClientAccessor.getHttpClient(destination),
132+
tokenCacheConfiguration);
119133
}
120134
catch( final ClassCastException e ) {
121135
final String msg =
@@ -214,6 +228,10 @@ private void setAppTidInCaseOfIAS( @Nullable final String tenantId )
214228
// the IAS property supplier will have set this to the provider ID by default
215229
// we have to override it here to match the current tenant, if the current tenant is defined
216230
additionalParameters.put("app_tid", tenantId);
231+
if( onBehalfOf == OnBehalfOf.NAMED_USER_CURRENT_TENANT ) {
232+
// workaround until a fix is provided by IAS
233+
additionalParameters.put("refresh_token", "0");
234+
}
217235
}
218236
}
219237

@@ -335,6 +353,7 @@ static class Builder
335353
private TenantPropagationStrategy tenantPropagationStrategy = TenantPropagationStrategy.ZID_HEADER;
336354
private final Map<String, String> additionalParameters = new HashMap<>();
337355
private ResilienceConfiguration.TimeLimiterConfiguration timeLimiter = OAuth2Options.DEFAULT_TIMEOUT;
356+
private TokenCacheParameters tokenCacheParameters = OAuth2Options.DEFAULT_TOKEN_CACHE_PARAMETERS;
338357

339358
@Nonnull
340359
Builder withTokenUri( @Nonnull final String tokenUri )
@@ -412,6 +431,13 @@ Builder withTimeLimiter( @Nonnull final ResilienceConfiguration.TimeLimiterConfi
412431
return this;
413432
}
414433

434+
@Nonnull
435+
Builder withTokenCacheParameters( @Nonnull final TokenCacheParameters tokenCacheParameters )
436+
{
437+
this.tokenCacheParameters = tokenCacheParameters;
438+
return this;
439+
}
440+
415441
@Nonnull
416442
OAuth2Service build()
417443
{
@@ -434,13 +460,15 @@ OAuth2Service build()
434460

435461
// copy the additional parameters to prevent accidental manipulation after the `OAuth2Service` instance has been created.
436462
final Map<String, String> additionalParameters = new HashMap<>(this.additionalParameters);
463+
437464
return new OAuth2Service(
438465
tokenUri,
439466
identity,
440467
onBehalfOf,
441468
tenantPropagationStrategy,
442469
additionalParameters,
443-
resilienceConfig);
470+
resilienceConfig,
471+
tokenCacheParameters);
444472
}
445473
}
446474

cloudplatform/connectivity-oauth/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/OAuth2ServiceBindingDestinationLoader.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ DestinationHeaderProvider createHeaderProvider(
326326
.withTenantPropagationStrategyFrom(serviceIdentifier)
327327
.withAdditionalParameters(oAuth2Options.getAdditionalTokenRetrievalParameters())
328328
.withTimeLimiter(oAuth2Options.getTimeLimiter())
329+
.withTokenCacheParameters(oAuth2Options.getTokenCacheParameters())
329330
.build();
330331
return new OAuth2HeaderProvider(oAuth2Service, authHeader);
331332
}

cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultOAuth2PropertySupplierTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,29 @@ void testTimeoutConfiguration()
234234
.isEqualTo(TimeLimiterConfiguration.of(Duration.ofSeconds(100)));
235235
}
236236

237+
@Test
238+
void testTokenCacheConfigurationOption()
239+
{
240+
final ServiceBinding binding =
241+
new ServiceBindingBuilder(ServiceIdentifier.DESTINATION).with("name", "asdf").build();
242+
ServiceBindingDestinationOptions options = ServiceBindingDestinationOptions.forService(binding).build();
243+
244+
sut = new DefaultOAuth2PropertySupplier(options);
245+
246+
assertThat(sut.getOAuth2Options().getTokenCacheParameters())
247+
.isSameAs(OAuth2Options.DEFAULT_TOKEN_CACHE_PARAMETERS);
248+
249+
options =
250+
ServiceBindingDestinationOptions
251+
.forService(binding)
252+
.withOption(OAuth2Options.TokenCacheParameters.of(Duration.ofSeconds(10), 5, Duration.ofSeconds(1)))
253+
.build();
254+
sut = new DefaultOAuth2PropertySupplier(options);
255+
assertThat(sut.getOAuth2Options().getTokenCacheParameters())
256+
.usingRecursiveComparison()
257+
.isEqualTo(OAuth2Options.TokenCacheParameters.of(Duration.ofSeconds(10), 5, Duration.ofSeconds(1)));
258+
}
259+
237260
@RequiredArgsConstructor
238261
private static final class ServiceBindingBuilder
239262
{

cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/OAuth2ServiceTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ void testSubdomainTenantStrategy()
226226
1,
227227
postRequestedFor(urlEqualTo("/oauth/token"))
228228
.withRequestBody(containing("app_tid=tenant"))
229+
.withRequestBody(containing("refresh_token=0"))
229230
.withRequestBody(
230231
containing("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer".replace(":", "%3A")))
231232
.withRequestBody(containing("assertion=" + token)));

release_notes.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212

1313
### ✨ New Functionality
1414

15-
-
15+
- Add `TokenCacheParameters` to `OAuth2Options` to configurate token cache duration, expiration delta and cache size.
1616

1717
### 📈 Improvements
1818

19-
-
19+
- Relax OAuth2 token cache duration to 1hr to avoid unnecessary token refreshes.
20+
- Disable refresh tokens when obtaining user tokens from IAS.
21+
This acts as a workaround for a limitation of IAS, where obtaining a refresh token invalidates the original token.
2022

2123
### 🐛 Fixed Issues
2224

2325
- OData v2 and OData v4: Fixes eager HTTP response evaluation for _Create_, _Update_, and _Delete_ request builders in convenience APIs.
24-
Previous change of `5.20.0` may have resulted in the HTTP connection being left open after the request was executed.
26+
Previous change of `5.20.0` may have resulted in the HTTP connection being left open after the request was executed.

0 commit comments

Comments
 (0)