Core: Fix child AuthSession inheriting parent's expiresAtMillis#15999
Core: Fix child AuthSession inheriting parent's expiresAtMillis#15999bharos wants to merge 2 commits intoapache:mainfrom
Conversation
When creating a child AuthSession via fromTokenResponse() or fromAccessToken(), the AuthConfig builder uses .from(parent.config()) to inherit configuration. This copies the parent's expiresAtMillis value, which overrides the @Value.Default that would otherwise recompute from the new child token's JWT exp claim. This causes child sessions to schedule their first token refresh based on the parent's token expiry rather than their own. In token-exchange flows where a catalog session has a long-lived token (e.g., 1 hour) and the exchanged per-user token is short-lived (e.g., 5 minutes), refresh is scheduled ~54 minutes out instead of ~4.5 minutes, causing the short-lived token to expire unrefreshed. Fix: Explicitly set expiresAtMillis from the child token in both fromTokenResponse() and fromAccessToken() builders, so the child session's expiry is always derived from its own token.
b76cbd5 to
711b09d
Compare
|
@adutra can you please review this change (or kindly let know who can do a review). Thanks! |
| .from(config()) | ||
| .token(response.token()) | ||
| .tokenType(response.issuedTokenType()) | ||
| .expiresAtMillis(OAuth2Util.expiresAtMillis(response.token())) |
There was a problem hiding this comment.
This change is good imho, but untested – it would be good to have it tested, although I agree it's not easy to test.
There was a problem hiding this comment.
Added refreshUsesRefreshedTokenExpiry() test
| private static String tokenWithExp(long expSeconds) { | ||
| String header = "{\"alg\":\"none\",\"typ\":\"JWT\"}"; | ||
| String payload = "{\"sub\":\"test\",\"exp\":" + expSeconds + "}"; | ||
| Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); | ||
| return encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) | ||
| + "." | ||
| + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) | ||
| + "." | ||
| + encoder.encodeToString("fakesig".getBytes(StandardCharsets.UTF_8)); | ||
| } |
There was a problem hiding this comment.
Can be replaced with Nimbus Jose (which comes transitively through MockServer and other deps):
| private static String tokenWithExp(long expSeconds) { | |
| String header = "{\"alg\":\"none\",\"typ\":\"JWT\"}"; | |
| String payload = "{\"sub\":\"test\",\"exp\":" + expSeconds + "}"; | |
| Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); | |
| return encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) | |
| + "." | |
| + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) | |
| + "." | |
| + encoder.encodeToString("fakesig".getBytes(StandardCharsets.UTF_8)); | |
| } | |
| private static String tokenWithExp(long expSeconds) { | |
| JWTClaimsSet claimsSet = | |
| new JWTClaimsSet.Builder().subject("test").claim("exp", expSeconds).build(); | |
| return new PlainJWT(claimsSet).serialize(); | |
| } |
There was a problem hiding this comment.
Good point — replaced manual Base64 JWT construction with Nimbus PlainJWT
| String parentToken = tokenWithExp(7200); | ||
| // child token with no exp claim (opaque) | ||
| String childToken = "opaque-access-token"; | ||
|
|
||
| AuthConfig parentConfig = | ||
| AuthConfig.builder() | ||
| .token(parentToken) | ||
| .tokenType(OAuth2Properties.ACCESS_TOKEN_TYPE) | ||
| .keepRefreshed(false) | ||
| .build(); | ||
| OAuth2Util.AuthSession parent = new OAuth2Util.AuthSession(Map.of(), parentConfig); | ||
| assertThat(parent.expiresAtMillis()).isEqualTo(TimeUnit.SECONDS.toMillis(7200)); | ||
|
|
||
| OAuthTokenResponse response = | ||
| OAuthTokenResponse.builder() | ||
| .withToken(childToken) | ||
| .withTokenType(BEARER) | ||
| .withIssuedTokenType(OAuth2Properties.ACCESS_TOKEN_TYPE) | ||
| .setExpirationInSeconds(600) | ||
| .build(); |
There was a problem hiding this comment.
i wonder if we can extract these common block in a helper private function below as creating partent token and creating a OAuthTokenResponse from the child token etc .....
There was a problem hiding this comment.
Done — extracted parentSession(), childTokenResponse(), and tokenWithExp() helpers.
- Replace manual Base64 JWT construction with Nimbus PlainJWT - Extract parentSession(), childTokenResponse(), tokenWithExp() helpers - Add refreshUsesRefreshedTokenExpiry test for refresh() code path - Import OAuth2Util.AuthSession to avoid FQN usage in tests
|
Thanks for the review @adutra and @singhpk234 , addressed your comments. PTAL |
What
Fix child
AuthSessioninheriting the parent session'sexpiresAtMillisinstead of computing it from its own token.Why
When creating a child
AuthSessionviafromTokenResponse()orfromAccessToken(), theAuthConfigbuilder calls.from(parent.config())to inherit configuration (scope, credential, oauth2ServerUri, etc.). However, Immutables'from()copies all values from the parent, includingexpiresAtMillis. This overrides the@Value.Defaultmethod that would otherwise recompute the expiry from the child's own JWTexpclaim.Impact: The child session schedules its first token refresh based on the parent's token expiry, not its own. In token exchange flows where:
...the refresh is scheduled ~54 minutes out instead of ~4.5 minutes, causing the short-lived token to expire without being refreshed.
How
Explicitly set
expiresAtMillisfrom the child token in bothfromTokenResponse()andfromAccessToken()builders:This ensures the child session's expiry is always derived from its own token's JWT
expclaim (ornullfor opaque tokens, falling back toresponse.expiresInSeconds()for scheduling).Fixes #16002
Testing
Added 3 tests to
TestOAuth2Util:fromTokenResponseUsesChildTokenExpiry— verifies child JWT token's exp is used, not parent'sfromTokenResponseOpaqueTokenDoesNotInheritParentExpiry— verifies opaque child tokens don't inherit parent's expfromAccessTokenUsesChildTokenExpiry— verifies the same fix forfromAccessToken()