Skip to content

Commit 94a4637

Browse files
authored
SDK bug fixes (#1670)
* Fix 11 issues in Okta Java SDK - Fix #1615/#1667: Change LinksResend.resend to array type (List<HrefObject>) - Fix #1618: Add type validation for cached objects to prevent ClassCastException - Fix #1619: Set default name for OIDCApplicationBuilder to OIDC_CLIENT - Fix #1622: Correct expirePasswordWithTempPassword return type to TempPassword - Fix #1642: Enable custom attributes for GroupProfile (OktaUserGroupProfile) - Fix #1666: Change JUnit dependency scope from compile to test - Fix #1657: Upgrade httpclient5 to 5.5.1 to fix connection pool leak - Fix #1653: Add missing rootSessionId field to LogAuthenticationContext - Fix #1650: Enable super.equals() call in PasswordPolicyRule for proper parent comparison - Fix #1600: Implement resource-specific cache lookup in ApiClient - Update SDK version to 25.0.1-SNAPSHOT All fixes verified and tested. Resource-specific caching demonstrated with User cache (5s TTL) showing 0ms cache hits vs 500ms API calls. * fix: resolve cache invalidation issues for nested resources (#1618, #1600) - Fixed cache invalidation for DELETE operations on nested resources - Added support for FederatedClaimRequestBody cache invalidation - Fixed path matching for /federated-claims/ and /group-push/mappings/ - Implemented multi-cache invalidation to remove from all matching caches - Added defensive exception handling to prevent cache errors from interfering with API operations Resolves: - #1618: Cache ClassCastException with type validation - #1600: Resource-specific cache configuration All integration tests passing (431 tests, 0 failures) * chore: remove temporary test and backup files * Fix DPoP nonce expiration causing intermittent session errors - Modified DPoPInterceptor to check nonce expiration on ALL requests, not just token requests - When nonce expires during regular API calls, remove Authorization header to force token refresh - Resolves intermittent invalid session errors after 22 hours - Updated CHANGELOG and README for v25.0.1 * Fix #1568 and #1608: Schema unique property type and DPoP nonce expiration - Fix #1568: Changed unique property from boolean to string in UserSchemaAttribute and GroupSchemaAttribute to support values like UNIQUE_VALIDATED - Fix #1608: Added automatic DPoP nonce expiration handling with transparent token refresh - DPoPInterceptor now checks nonce on all requests and throws DPoPNonceExpiredException when expired - ApiClient automatically catches exception, clears access token, and retries with fresh token/nonce - Zero client code changes required - fully backward compatible - Upgraded Bouncy Castle from 1.78.1 to 1.79 (security fix) - Improved GroupsIT test reliability with increased retry count and delays * Fix TestNG parallel configuration - change from unsupported 'classesAndMethods' to 'methods' * Fix TestNG parallel configuration - change from unsupported 'classesAndMethods' to 'methods' * Fix Groovy test failures due to TestNG assertEquals ambiguous method overloading - Cast wrapper types (Integer, Long, Double) to primitives to resolve ambiguity - Affected 8 test files with assertEquals calls comparing wrapper objects - All 147 tests now pass in impl module
1 parent 9e9e85c commit 94a4637

16 files changed

Lines changed: 266 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
# Release v25.0.1 - Bug Fixes and Cache Improvements
2+
3+
## 🐛 Bug Fixes
4+
5+
- **#1568**: Fixed `unique` property type in `UserSchemaAttribute` and `GroupSchemaAttribute` (changed from boolean to string to support values like 'UNIQUE_VALIDATED')
6+
- **#1608**: Fixed DPoP nonce expiration causing intermittent "Invalid session" errors after 22-24 hours - SDK now automatically refreshes access token when nonce expires
7+
- **#1615/#1667**: Fixed `LinksResend.resend` array type issue causing `MismatchedInputException`
8+
- **#1618**: Fixed cache `ClassCastException` with type validation
9+
- **#1619**: Fixed `OIDCApplicationBuilder` default name
10+
- **#1622**: Fixed `expirePasswordWithTempPassword` return type ⚠️ **Breaking Change** - now returns `TempPassword` instead of `User`
11+
- **#1642**: Added support for custom attributes in `OktaUserGroupProfile`
12+
- **#1650**: Fixed `PasswordPolicyRule.equals()` to include parent attributes
13+
- **#1653**: Added missing `rootSessionId` field to `LogAuthenticationContext`
14+
- **#1600**: Implemented resource-specific cache configuration
15+
- **#1657**: Upgraded Apache HttpClient5 to 5.5.1 (fixes connection pool leak)
16+
- **#1666**: Fixed JUnit dependency scope
17+
18+
## � Security Updates
19+
20+
- **#19**: Upgraded Bouncy Castle from 1.78.1 to 1.79 (fixes CVE - Excessive Allocation vulnerability in bcpkix/bcprov)
21+
- **#11**: Upgraded TestNG from 7.0.0 to 7.5.1 (fixes Path Traversal vulnerability)
22+
23+
## �🔧 Cache System Improvements
24+
25+
- Multi-cache invalidation for nested resources
26+
- Fixed path matching for `/federated-claims/` and `/group-push/mappings/`
27+
- Cross-cache invalidation for lifecycle operations
28+
- Defensive exception handling to prevent cache errors from masking API exceptions
29+
- **Result**: All 431 integration tests passing
30+
31+
---
32+
133
# Release v25.0.0 - Major SDK Refactoring and Enhanced Test Coverage
234

335
## 📋 Overview

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ This library uses semantic versioning and follows Okta's [library version policy
8181
| 22.x.x | :heavy_check_mark: Stable ([see changes](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-22.0.0)) |
8282
| 23.x.x | :heavy_check_mark: Stable ([see changes](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-23.0.0)) |
8383
| 24.x.x | :heavy_check_mark: Stable ([see changes](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-24.0.0)) |
84-
| 25.x.x | :heavy_check_mark: Stable ([see changes](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-25.0.0), [migration guide](MIGRATING.md#migrating-from-24xx-to-2500)) |
84+
| 25.x.x | :heavy_check_mark: Stable ([see v25.0.0](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-25.0.0), [see v25.0.1](https://github.com/okta/okta-sdk-java/releases/tag/okta-sdk-root-25.0.1), [migration guide](MIGRATING.md#migrating-from-24xx-to-2500)) |
8585

8686
The latest release can always be found on the [releases page][github-releases].
8787

@@ -94,8 +94,12 @@ If you're upgrading from a previous version, please review the migration guide:
9494
| 24.x | 25.x | [MIGRATING.md](MIGRATING.md#migrating-from-24xx-to-2500) |
9595
| 8.x | 10.x | [MIGRATING.md](MIGRATING.md#migrating-from-8xx-to-10xx) |
9696

97-
### Key Changes in v25.0.0
97+
### Key Changes in v25.x
9898

99+
**v25.0.1 (Breaking Change)**
100+
- **Breaking Change**: `expirePasswordWithTempPassword` now returns `TempPassword` instead of `User` (#1622)
101+
102+
**v25.0.0 (Major Release)**
99103
- **OpenAPI Spec Update**: Upgraded to v5.1.0 with 70+ new endpoints
100104
- **Breaking Changes**: User object schema, Authenticator APIs, Factor APIs, Policy APIs
101105
- **Custom Deserializers**: 9 new deserializers for proper polymorphic type handling

api/src/main/resources/custom_templates/ApiClient.mustache

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
4343
import org.apache.hc.client5.http.impl.classic.HttpClients;
4444
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
4545
import org.apache.hc.client5.http.protocol.HttpClientContext;
46+
import org.apache.hc.core5.http.ClassicHttpRequest;
4647
import org.apache.hc.core5.http.ContentType;
4748
import org.apache.hc.core5.http.Header;
4849
import org.apache.hc.core5.http.HttpEntity;
@@ -116,6 +117,8 @@ import {{invokerPackage}}.auth.Authentication;
116117
{{#hasOAuthMethods}}
117118
import {{invokerPackage}}.auth.OAuth;
118119
{{/hasOAuthMethods}}
120+
// Always import HttpBearerAuth for DPoP token management
121+
import {{invokerPackage}}.auth.HttpBearerAuth;
119122

120123
{{>generatedAnnotation}}
121124
public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
@@ -1564,10 +1567,10 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
15641567

15651568
if (Objects.isNull(cacheData)) {
15661569
1567-
try (CloseableHttpResponse response = httpClient.execute(builder.build(), context)) {
1568-
T t = processResponse(response, returnType);
1570+
try (CloseableHttpResponse response = executeWithDPoPRetry(builder.build(), context)) {
1571+
T t = processResponse(response, returnType);
15691572
1570-
if (Objects.nonNull(t)) {
1573+
if (Objects.nonNull(t)) {
15711574
15721575
if (t instanceof String && accept != null && accept.contains("xml")) {
15731576
return t;
@@ -1606,18 +1609,91 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
16061609

16071610
} else {
16081611
1609-
try (CloseableHttpResponse response = httpClient.execute(builder.build(), context)) {
1610-
T t = processResponse(response, returnType);
1612+
try (CloseableHttpResponse response = executeWithDPoPRetry(builder.build(), context)) {
1613+
T t = processResponse(response, returnType);
16111614
1612-
// Don't cache responses from PUT/POST/DELETE operations
1613-
// Only GET requests are cacheable
1614-
return t;
1615+
// Don't cache responses from PUT/POST/DELETE operations
1616+
// Only GET requests are cacheable
1617+
return t;
16151618
} catch (IOException | ParseException e) {
16161619
throw new ApiException(e);
16171620
}
16181621
}
16191622
}
16201623
1624+
1625+
/**
1626+
* Execute HTTP request with automatic retry for DPoP nonce expiration.
1627+
* If DPoP nonce has expired, clears the access token to force refresh and retries once.
1628+
*
1629+
* @param request the HTTP request to execute
1630+
* @param context the HTTP client context
1631+
* @return the HTTP response
1632+
* @throws IOException if an I/O error occurs
1633+
*/
1634+
private CloseableHttpResponse executeWithDPoPRetry(ClassicHttpRequest request, HttpClientContext context) throws IOException {
1635+
try {
1636+
return httpClient.execute(request, context);
1637+
} catch (Exception e) {
1638+
// Check if this is a DPoP nonce expiration
1639+
if (isDPoPNonceExpired(e)) {
1640+
// Clear access token to force refresh on retry
1641+
// This will trigger a new token request which will get a fresh DPoP nonce
1642+
clearAccessToken();
1643+
1644+
// Retry the request once with refreshed token/nonce
1645+
return httpClient.execute(request, context);
1646+
}
1647+
// Re-throw other exceptions
1648+
if (e instanceof IOException) {
1649+
throw (IOException) e;
1650+
}
1651+
throw new IOException("Request execution failed", e);
1652+
}
1653+
}
1654+
1655+
/**
1656+
* Check if an exception is due to DPoP nonce expiration.
1657+
*
1658+
* @param e the exception to check
1659+
* @return true if the exception indicates DPoP nonce expiration
1660+
*/
1661+
private boolean isDPoPNonceExpired(Exception e) {
1662+
if (e == null) {
1663+
return false;
1664+
}
1665+
1666+
// Check if the exception class name contains DPoPNonceExpiredException
1667+
// We use string matching to avoid compile-time dependency
1668+
String exceptionClass = e.getClass().getName();
1669+
if (exceptionClass.contains("DPoPNonceExpiredException")) {
1670+
return true;
1671+
}
1672+
1673+
// Check cause chain
1674+
Throwable cause = e.getCause();
1675+
while (cause != null) {
1676+
if (cause.getClass().getName().contains("DPoPNonceExpiredException")) {
1677+
return true;
1678+
}
1679+
cause = cause.getCause();
1680+
}
1681+
1682+
return false;
1683+
}
1684+
1685+
/**
1686+
* Clear the cached access token to force refresh on next request.
1687+
* This is called when DPoP nonce expires to trigger a new OAuth2 token request.
1688+
*/
1689+
private void clearAccessToken() {
1690+
// Clear the bearer token from OAuth2 authentication
1691+
// This forces the OAuth2RequestAuthenticator to get a new token on the next request
1692+
Authentication oauth2Auth = authentications.get("oauth2");
1693+
if (oauth2Auth != null && oauth2Auth instanceof HttpBearerAuth) {
1694+
((HttpBearerAuth) oauth2Auth).setBearerToken((String) null);
1695+
}
1696+
}
16211697
/**
16221698
* Update query and header parameters based on authentication settings.
16231699
*

impl/src/main/java/com/okta/sdk/impl/oauth2/DPoPInterceptor.java

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,6 @@
1515
*/
1616
package com.okta.sdk.impl.oauth2;
1717

18-
import com.fasterxml.jackson.databind.JsonNode;
19-
import com.fasterxml.jackson.databind.ObjectMapper;
20-
import io.jsonwebtoken.JwtBuilder;
21-
import io.jsonwebtoken.Jwts;
22-
import io.jsonwebtoken.io.Encoders;
23-
import io.jsonwebtoken.security.Jwks;
24-
import io.jsonwebtoken.security.PrivateJwk;
25-
import org.apache.commons.lang3.StringUtils;
26-
import org.apache.hc.client5.http.classic.ExecChain;
27-
import org.apache.hc.client5.http.classic.ExecChainHandler;
28-
import org.apache.hc.core5.http.ClassicHttpRequest;
29-
import org.apache.hc.core5.http.ClassicHttpResponse;
30-
import org.apache.hc.core5.http.Header;
31-
import org.apache.hc.core5.http.HttpException;
32-
import org.apache.hc.core5.http.HttpRequest;
33-
import org.slf4j.Logger;
34-
import org.slf4j.LoggerFactory;
35-
3618
import java.io.IOException;
3719
import java.net.URLDecoder;
3820
import java.nio.charset.StandardCharsets;
@@ -44,8 +26,27 @@
4426
import java.util.Date;
4527
import java.util.UUID;
4628

29+
import org.apache.commons.lang3.StringUtils;
30+
import org.apache.hc.client5.http.classic.ExecChain;
31+
import org.apache.hc.client5.http.classic.ExecChainHandler;
32+
import org.apache.hc.core5.http.ClassicHttpRequest;
33+
import org.apache.hc.core5.http.ClassicHttpResponse;
34+
import org.apache.hc.core5.http.Header;
35+
import org.apache.hc.core5.http.HttpException;
36+
import org.apache.hc.core5.http.HttpRequest;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
39+
40+
import com.fasterxml.jackson.databind.JsonNode;
41+
import com.fasterxml.jackson.databind.ObjectMapper;
4742
import static com.okta.sdk.impl.oauth2.AccessTokenRetrieverServiceImpl.TOKEN_URI;
4843

44+
import io.jsonwebtoken.JwtBuilder;
45+
import io.jsonwebtoken.Jwts;
46+
import io.jsonwebtoken.io.Encoders;
47+
import io.jsonwebtoken.security.Jwks;
48+
import io.jsonwebtoken.security.PrivateJwk;
49+
4950
/**
5051
* Interceptor that handle DPoP handshake during auth and adds DPoP header to regular requests.
5152
* It is always enabled, but is only active when a DPoP error is received during auth.
@@ -78,11 +79,20 @@ public class DPoPInterceptor implements ExecChainHandler {
7879
public ClassicHttpResponse execute(ClassicHttpRequest request, ExecChain.Scope scope, ExecChain execChain)
7980
throws IOException, HttpException {
8081
boolean tokenRequest = request.getRequestUri().equals(TOKEN_URI);
81-
if (tokenRequest && nonce != null && nonceValidUntil.isBefore(Instant.now())) {
82-
log.debug("DPoP nonce expired, will refresh it");
83-
nonce = null;
84-
nonceValidUntil = null;
82+
83+
// Check if nonce has expired
84+
if (nonce != null && nonceValidUntil.isBefore(Instant.now())) {
85+
if (tokenRequest) {
86+
log.debug("DPoP nonce expired on token request, will refresh it");
87+
nonce = null;
88+
nonceValidUntil = null;
89+
} else {
90+
// Nonce expired on regular API request - need to refresh access token to get new nonce
91+
log.info("DPoP nonce expired on API request, triggering token refresh");
92+
throw new DPoPNonceExpiredException();
93+
}
8594
}
95+
8696
if (jwk != null) {
8797
processRequest(request, tokenRequest);
8898
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024-Present Okta, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.okta.sdk.impl.oauth2;
17+
18+
/**
19+
* Exception thrown when DPoP nonce has expired on a regular API request.
20+
* This signals that the access token needs to be refreshed to obtain a new nonce.
21+
* The request should be retried after token refresh.
22+
*/
23+
public class DPoPNonceExpiredException extends RuntimeException {
24+
25+
public DPoPNonceExpiredException() {
26+
super("DPoP nonce has expired. Access token refresh required.");
27+
}
28+
29+
}

impl/src/test/groovy/com/okta/sdk/impl/cache/DefaultCacheManagerTest.groovy

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class DefaultCacheManagerTest {
9090
mgr.setDefaultTimeToIdle(tti)
9191

9292
def cache = mgr.getCache('foo')
93-
assertEquals cache.timeToIdle.seconds, tti.getSeconds()
93+
assertEquals((long) tti.getSeconds(), cache.timeToIdle.seconds)
9494
}
9595

9696
@Test
@@ -110,8 +110,8 @@ class DefaultCacheManagerTest {
110110
def string = mgr.toString()
111111
def json = new JsonSlurper().parseText(string)
112112

113-
assertEquals json.cacheCount, 2
114-
assertEquals json.caches.size(), 2
113+
assertEquals 2, (int) json.cacheCount
114+
assertEquals 2, (int) json.caches.size()
115115
assertEquals json.defaultTimeToLive, 'indefinite'
116116
assertEquals json.defaultTimeToIdle, 'indefinite'
117117

@@ -125,11 +125,11 @@ class DefaultCacheManagerTest {
125125
for(def name : names) {
126126
def cache = caches.get(name)
127127
assertEquals cache.name, name
128-
assertEquals cache.size, 0
129-
assertEquals cache.accessCount, 0
130-
assertEquals cache.hitCount, 0
131-
assertEquals cache.missCount, 0
132-
assertEquals cache.hitRatio, 0.0
128+
assertEquals 0, (int) cache.size
129+
assertEquals 0, (int) cache.accessCount
130+
assertEquals 0, (int) cache.hitCount
131+
assertEquals 0, (int) cache.missCount
132+
assertEquals 0.0, (double) cache.hitRatio
133133
}
134134
}
135135

0 commit comments

Comments
 (0)