Skip to content

Commit

Permalink
Repeat the grant request if OidcClient refresh token has expired
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Dec 3, 2021
1 parent cd8fde5 commit 012a41f
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,17 @@ public String getGrantType() {
public String refreshTokenProperty = OidcConstants.REFRESH_TOKEN_VALUE;

/**
* Refresh token property name in a token grant response
* Access token expiry property name in a token grant response
*/
@ConfigItem(defaultValue = OidcConstants.EXPIRES_IN)
public String expiresInProperty = OidcConstants.EXPIRES_IN;

/**
* Refresh token expiry property name in a token grant response
*/
@ConfigItem(defaultValue = OidcConstants.REFRESH_EXPIRES_IN)
public String refreshExpiresInProperty = OidcConstants.REFRESH_EXPIRES_IN;

public Type getType() {
return type;
}
Expand Down Expand Up @@ -149,6 +155,14 @@ public String getExpiresInProperty() {
public void setExpiresInProperty(String expiresInProperty) {
this.expiresInProperty = expiresInProperty;
}

public String getRefreshExpiresInProperty() {
return refreshExpiresInProperty;
}

public void setRefreshExpiresInProperty(String refreshExpiresInProperty) {
this.refreshExpiresInProperty = refreshExpiresInProperty;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ public class Tokens {
final private Long accessTokenExpiresAt;
final private Long refreshTokenTimeSkew;
final private String refreshToken;
final Long refreshTokenExpiresAt;
final private JsonObject grantResponse;

public Tokens(String accessToken, Long accessTokenExpiresAt, Duration refreshTokenTimeSkewDuration, String refreshToken,
JsonObject grantResponse) {
Long refreshTokenExpiresAt, JsonObject grantResponse) {
this.accessToken = accessToken;
this.accessTokenExpiresAt = accessTokenExpiresAt;
this.refreshTokenTimeSkew = refreshTokenTimeSkewDuration == null ? null : refreshTokenTimeSkewDuration.getSeconds();
this.refreshToken = refreshToken;
this.refreshTokenExpiresAt = refreshTokenExpiresAt;
this.grantResponse = grantResponse;
}

Expand All @@ -44,11 +46,11 @@ public Long getRefreshTokenTimeSkew() {
}

public boolean isAccessTokenExpired() {
if (accessTokenExpiresAt == null) {
return false;
}
final long nowSecs = System.currentTimeMillis() / 1000;
return nowSecs > accessTokenExpiresAt;
return isExpired(accessTokenExpiresAt);
}

public boolean isRefreshTokenExpired() {
return isExpired(refreshTokenExpiresAt);
}

public boolean isAccessTokenWithinRefreshInterval() {
Expand All @@ -58,4 +60,12 @@ public boolean isAccessTokenWithinRefreshInterval() {
final long nowSecs = System.currentTimeMillis() / 1000;
return nowSecs + refreshTokenTimeSkew > accessTokenExpiresAt;
}

private static boolean isExpired(Long expiresAt) {
if (expiresAt == null) {
return false;
}
final long nowSecs = System.currentTimeMillis() / 1000;
return nowSecs > expiresAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,16 @@ private Tokens emitGrantTokens(HttpResponse<Buffer> resp, boolean refresh) {
if (resp.statusCode() == 200) {
LOG.debugf("%s OidcClient has %s the tokens", oidcConfig.getId().get(), (refresh ? "refreshed" : "acquired"));
JsonObject json = resp.bodyAsJsonObject();
// access token
final String accessToken = json.getString(oidcConfig.grant.accessTokenProperty);
final Long accessTokenExpiresAt = getExpiresAtValue(accessToken, json.getValue(oidcConfig.grant.expiresInProperty));

final String refreshToken = json.getString(oidcConfig.grant.refreshTokenProperty);
final Object expiresInValue = json.getValue(oidcConfig.grant.expiresInProperty);
Long accessTokenExpiresAt;
if (expiresInValue != null) {
long accessTokenExpiresIn = expiresInValue instanceof Number ? ((Number) expiresInValue).longValue()
: Long.parseLong(expiresInValue.toString());
accessTokenExpiresAt = oidcConfig.absoluteExpiresIn ? accessTokenExpiresIn
: Instant.now().getEpochSecond() + accessTokenExpiresIn;
} else {
accessTokenExpiresAt = getExpiresJwtClaim(accessToken);
}
final Long refreshTokenExpiresAt = getExpiresAtValue(refreshToken,
json.getValue(oidcConfig.grant.refreshExpiresInProperty));

return new Tokens(accessToken, accessTokenExpiresAt, oidcConfig.refreshTokenTimeSkew.orElse(null), refreshToken,
json);
refreshTokenExpiresAt, json);
} else {
String errorMessage = resp.bodyAsString();
LOG.debugf("%s OidcClient has failed to complete the %s grant request: status: %d, error message: %s",
Expand All @@ -147,8 +143,19 @@ private Tokens emitGrantTokens(HttpResponse<Buffer> resp, boolean refresh) {
}
}

private static Long getExpiresJwtClaim(String accessToken) {
JsonObject claims = decodeJwtToken(accessToken);
private Long getExpiresAtValue(String token, Object expiresInValue) {
if (expiresInValue != null) {
long tokenExpiresIn = expiresInValue instanceof Number ? ((Number) expiresInValue).longValue()
: Long.parseLong(expiresInValue.toString());
return oidcConfig.absoluteExpiresIn ? tokenExpiresIn
: Instant.now().getEpochSecond() + tokenExpiresIn;
} else {
return token != null ? getExpiresJwtClaim(token) : null;
}
}

private static Long getExpiresJwtClaim(String token) {
JsonObject claims = decodeJwtToken(token);
if (claims != null) {
try {
return claims.getLong(Claims.exp.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ public Uni<Tokens> getTokens(OidcClient oidcClient) {
} else {
Tokens tokens = currentState.tokens;
if (tokens.isAccessTokenExpired() || tokens.isAccessTokenWithinRefreshInterval()) {
newState = new TokenRequestState(prepareUni(tokens.getRefreshToken() != null
? oidcClient.refreshTokens(tokens.getRefreshToken())
: oidcClient.getTokens()));
newState = new TokenRequestState(
prepareUni((tokens.getRefreshToken() != null && !tokens.isRefreshTokenExpired())
? oidcClient.refreshTokens(tokens.getRefreshToken())
: oidcClient.getTokens()));
if (tokenRequestStateUpdater.compareAndSet(this, currentState, newState)) {
return newState.tokenUni;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ public final class OidcConstants {
public static final String EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";

public static final String EXPIRES_IN = "expires_in";
public static final String REFRESH_EXPIRES_IN = "refresh_expires_in";
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Map<String, String> start() {
.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(
"{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
"{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_2\", \"refresh_expires_in\":1}")));

server.stubFor(WireMock.post("/refresh-token-only")
.withRequestBody(matching("grant_type=refresh_token&refresh_token=shared_refresh_token"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,41 @@ public class OidcClientTest {

@Test
public void testEchoAndRefreshTokens() {
// access_token_1 and refresh_token_1 are acquired using a password grant request.
// access_token_1 expires in 4 seconds, refresh_token_1 has no lifespan limit as no `refresh_expires_in` property is returned.
// "Default OidcClient has acquired the tokens" record is added to the log
RestAssured.when().get("/frontend/echoToken")
.then()
.statusCode(200)
.body(equalTo("access_token_1"));

// Wait until the access token has expired
// Wait until the access_token_1 has expired
waitUntillAccessTokenHasExpired();

// access_token_1 has expired, refresh_token_1 is assumed to be valid and used to acquire new access_token_2 and refresh_token_2 are acquired.
// access_token_2 expires in 4 seconds, but refresh_token_2 - in 1 sec - it will expire by the time access_token_2 has expired
// "Default OidcClient has refreshed the tokens" record is added to the log
RestAssured.when().get("/frontend/echoToken")
.then()
.statusCode(200)
.body(equalTo("access_token_2"));

// Wait until the access_token_2 has expired
waitUntillAccessTokenHasExpired();

// Both access_token_2 and refresh_token_2 have now expired therefore a password grant request is repeated,
// as opposed to using a refresh token grant.
// access_token_1 is returned again - as the same token URL and grant properties are used and Wiremock stub returns access_token_1
// 2nd "Default OidcClient has acquired the tokens" record is added to the log
RestAssured.when().get("/frontend/echoToken")
.then()
.statusCode(200)
.body(equalTo("access_token_1"));

checkLog();
}

private static void waitUntillAccessTokenHasExpired() {
long expiredTokenTime = System.currentTimeMillis() + 5000;
await().atMost(10, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(3))
Expand All @@ -45,12 +74,6 @@ public Boolean call() throws Exception {
return System.currentTimeMillis() > expiredTokenTime;
}
});

RestAssured.when().get("/frontend/echoToken")
.then()
.statusCode(200)
.body(equalTo("access_token_2"));
checkLog();
}

@Test
Expand Down Expand Up @@ -109,7 +132,7 @@ public void run() throws Throwable {

}
}
assertEquals(1, tokenAcquisitionCount,
assertEquals(2, tokenAcquisitionCount,
"Log file must contain a single OidcClientImpl token acquisition confirmation");
assertEquals(1, tokenRefreshedCount,
"Log file must contain a single OidcClientImpl token refresh confirmation");
Expand Down

0 comments on commit 012a41f

Please sign in to comment.