Skip to content

Commit

Permalink
Merge pull request #40025 from sberyozkin/oauth_internal_id_token_lif…
Browse files Browse the repository at this point in the history
…espan

Use OAuth2 access token expiry time to set an internal ID token age
  • Loading branch information
sberyozkin authored Apr 12, 2024
2 parents 08d36bd + abeab0f commit 253c02e
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1102,8 +1102,12 @@ To support the integration with such OAuth2 servers, `quarkus-oidc` needs to be
[NOTE]
====
Even though you configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` is generated to standardize the way `quarkus-oidc` operates.
You use an `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request.
In this case, the session lifespan is set to 5 minutes, which you can extend further as described in the <<session-management,session management>> section.
You use an internal `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request.
In this case, the `IdToken` age is set to the value of a standard `expires_in` property in the authorization code flow response.
You can use a `quarkus.oidc.authentication.internal-id-token-lifespan`property to customize the ID token age.
The default ID token age is 5 minutes.
, which you can extend further as described in the <<session-management,session management>> section.
This simplifies how you handle an application that supports multiple OIDC providers.
====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,94 @@ public class AuthorizationCodeTokens {
private String idToken;
private String accessToken;
private String refreshToken;
private Long accessTokenExpiresIn;

public AuthorizationCodeTokens() {
}

public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken) {
this.setIdToken(idToken);
this.setAccessToken(accessToken);
this.setRefreshToken(refreshToken);
this(idToken, accessToken, refreshToken, null);
}

public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn) {
this.idToken = idToken;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.accessTokenExpiresIn = accessTokenExpiresIn;
}

/**
* Get the ID token
*
* @return ID token
*/
public String getIdToken() {
return idToken;
}

/**
* Set the ID token
*
* @param idToken ID token
*/
public void setIdToken(String idToken) {
this.idToken = idToken;
}

/**
* Get the access token
*
* @return the access token
*/
public String getAccessToken() {
return accessToken;
}

/**
* Set the access token
*
* @param accessToken the access token
*/
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

/**
* Get the refresh token
*
* @return refresh token
*/
public String getRefreshToken() {
return refreshToken;
}

/**
* Set the refresh token
*
* @param refreshToken refresh token
*/
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

/**
* Get the access token expires_in value in seconds.
* It is relative to the time the access token is issued at.
*
* @return access token expires_in value in seconds.
*/
public Long getAccessTokenExpiresIn() {
return accessTokenExpiresIn;
}

/**
* Set the access token expires_in value in seconds.
* It is relative to the time the access token is issued at.
* This property is only checked when an authorization code flow grant completes and does not have to be persisted..
*
* @param accessTokenExpiresIn access token expires_in value in seconds.
*/
public void setAccessTokenExpiresIn(Long accessTokenExpiresIn) {
this.accessTokenExpiresIn = accessTokenExpiresIn;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,8 @@ public Uni<SecurityIdentity> apply(final AuthorizationCodeTokens tokens, final T
LOG.errorf("ID token is not available in the authorization code grant response");
return Uni.createFrom().failure(new AuthenticationCompletionException());
} else {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null));
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null,
tokens.getAccessTokenExpiresIn()));
internalIdToken = true;
}
} else {
Expand All @@ -788,7 +789,8 @@ public Uni<Void> apply(SecurityIdentity identity) {
if (internalIdToken
&& OidcUtils.cacheUserInfoInIdToken(resolver, configContext.oidcConfig)) {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig,
identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null));
identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null,
tokens.getAccessTokenExpiresIn()));
}
return processSuccessfulAuthentication(context, configContext,
tokens, idToken, identity);
Expand Down Expand Up @@ -890,7 +892,8 @@ private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedSta
return null;
}

private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken) {
private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken,
Long accessTokenExpiresInSecs) {
JwtClaimsBuilder builder = Jwt.claims();
if (currentIdToken != null) {
AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse(
Expand All @@ -908,6 +911,8 @@ private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo use
}
if (oidcConfig.authentication.internalIdTokenLifespan.isPresent()) {
builder.expiresIn(oidcConfig.authentication.internalIdTokenLifespan.get().getSeconds());
} else if (accessTokenExpiresInSecs != null) {
builder.expiresIn(accessTokenExpiresInSecs);
}
builder.audience(oidcConfig.getClientId().get());
return builder.jws().header(INTERNAL_IDTOKEN_HEADER, true)
Expand Down Expand Up @@ -936,7 +941,7 @@ public Uni<? extends Void> apply(Void t) {
if (configContext.oidcConfig.token.lifespanGrace.isPresent()) {
maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt();
}
if (configContext.oidcConfig.token.refreshExpired) {
if (configContext.oidcConfig.token.refreshExpired && tokens.getRefreshToken() != null) {
maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds();
}
final long sessionMaxAge = maxAge;
Expand Down Expand Up @@ -1247,7 +1252,8 @@ public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) {
tokens.setIdToken(currentIdToken);
}
} else {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken));
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken,
tokens.getAccessTokenExpiresIn()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,14 @@ private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse<Buffer>
final String idToken = json.getString(OidcConstants.ID_TOKEN_VALUE);
final String accessToken = json.getString(OidcConstants.ACCESS_TOKEN_VALUE);
final String refreshToken = json.getString(OidcConstants.REFRESH_TOKEN_VALUE);
return new AuthorizationCodeTokens(idToken, accessToken, refreshToken);
Long tokenExpiresIn = null;
Object tokenExpiresInObj = json.getValue(OidcConstants.EXPIRES_IN);
if (tokenExpiresInObj != null) {
tokenExpiresIn = tokenExpiresInObj instanceof Number ? ((Number) tokenExpiresInObj).longValue()
: Long.parseLong(tokenExpiresInObj.toString());
}

return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, tokenExpiresIn);
}

private UserInfo getUserInfo(HttpResponse<Buffer> resp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken");
assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE));

long issuedAt = idTokenClaims.getLong("iat");
long expiresAt = idTokenClaims.getLong("exp");
assertEquals(299, expiresAt - issuedAt);

Cookie sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken");
Date date = sessionCookie.getExpires();
assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 299 + 300);
// This test enables the token refresh, in this case the cookie age is extended by additional 5 mins
// to minimize the risk of the browser losing immediately after it has expired, for this cookie
// be returned to Quarkus, analyzed and refreshed
assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 299 + 300 + 3);

// refresh
Thread.sleep(3000);
textPage = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken");
Expand All @@ -292,6 +304,15 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken");
assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE));

issuedAt = idTokenClaims.getLong("iat");
expiresAt = idTokenClaims.getLong("exp");
assertEquals(305, expiresAt - issuedAt);

sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken");
date = sessionCookie.getExpires();
assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 305 + 300);
assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 305 + 300 + 3);

webClient.getCookieManager().clearCookies();
}

Expand Down Expand Up @@ -448,6 +469,7 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() {
.withBody("{\n" +
" \"access_token\": \""
+ OidcWiremockTestResource.getAccessToken("alice", Set.of()) + "\","
+ "\"expires_in\": 299,"
+ " \"refresh_token\": \"refresh1234\""
+ "}")));
wireMockServer
Expand All @@ -464,7 +486,8 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() {
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"access_token\": \""
+ OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\""
+ OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\","
+ "\"expires_in\": 305"
+ "}")));

}
Expand Down

0 comments on commit 253c02e

Please sign in to comment.