Skip to content

Commit 895ffe7

Browse files
committed
Generate ID token if issued ID token was not refreshed
1 parent daaf4da commit 895ffe7

File tree

3 files changed

+114
-28
lines changed

3 files changed

+114
-28
lines changed

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,16 +1375,19 @@ public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) {
13751375
}
13761376

13771377
if (tokens.getIdToken() == null) {
1378-
if (isIdTokenRequired(configContext) || !isInternalIdToken(currentIdToken, configContext)) {
1379-
if (!autoRefresh) {
1378+
if (autoRefresh) {
1379+
// Auto-refresh is triggered while current ID token is still valid, continue using it.
1380+
tokens.setIdToken(currentIdToken);
1381+
} else if (isIdTokenRequired(configContext)) {
1382+
LOG.debugf(
1383+
"Required ID token is not returned in the refresh token grant response, re-authentication is required");
1384+
throw new AuthenticationFailedException(tokenMap(currentIdToken));
1385+
} else {
1386+
if (!isInternalIdToken(currentIdToken, configContext)) {
13801387
LOG.debugf(
1381-
"ID token is not returned in the refresh token grant response, re-authentication is required");
1382-
throw new AuthenticationFailedException(tokenMap(currentIdToken));
1383-
} else {
1384-
// Auto-refresh is triggered while current ID token is still valid, continue using it.
1385-
tokens.setIdToken(currentIdToken);
1388+
"OIDC provider issued an ID token after the authorization code flow completion but did not refresh it,"
1389+
+ " an internal ID token will be generated");
13861390
}
1387-
} else {
13881391
tokens.setIdToken(generateInternalIdToken(configContext, null, currentIdToken,
13891392
tokens.getAccessTokenExpiresIn()));
13901393
}

integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static org.hamcrest.Matchers.equalTo;
77
import static org.hamcrest.Matchers.is;
88
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
910
import static org.junit.jupiter.api.Assertions.assertNotEquals;
1011
import static org.junit.jupiter.api.Assertions.assertNotNull;
1112
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -202,7 +203,7 @@ private void testTenantWebApp2(String webApp2SubPath, String expectedResult) thr
202203
}
203204

204205
@Test
205-
public void testCodeFlowRefreshTokens() throws IOException, InterruptedException {
206+
public void testCodeFlowRefreshTokensWhenIdTokenIsExpired() throws Exception {
206207
try (final WebClient webClient = createWebClient()) {
207208
HtmlPage page = webClient.getPage("http://localhost:8081/tenant-refresh/tenant-web-app-refresh/api/user");
208209
assertEquals("Sign in to quarkus-webapp", page.getTitleText());
@@ -213,6 +214,8 @@ public void testCodeFlowRefreshTokens() throws IOException, InterruptedException
213214

214215
Cookie sessionCookie = getSessionCookie(page.getWebClient(), "tenant-web-app-refresh");
215216
assertNotNull(sessionCookie);
217+
JsonObject jwtHeaders = getIdTokenHeaders(sessionCookie.getValue());
218+
assertFalse(jwtHeaders.getBoolean("internal", false));
216219

217220
Set<Cookie> atSessionCookies = getSessionAtCookie(page.getWebClient(), "tenant-web-app-refresh");
218221
assertEquals(3, atSessionCookies.size());
@@ -225,10 +228,66 @@ public void testCodeFlowRefreshTokens() throws IOException, InterruptedException
225228
+ ", refreshToken: true",
226229
page.getBody().asNormalizedText());
227230

228-
// Wait till the session expires - which should cause the first and also last token refresh request,
229-
// id and access tokens should have new values, refresh token value should remain the same.
230-
// No new sign-in process is required.
231-
//await().atLeast(6, TimeUnit.SECONDS);
231+
Thread.sleep(6 * 1000);
232+
233+
webClient.getOptions().setRedirectEnabled(false);
234+
WebResponse webResponse = webClient
235+
.loadWebResponse(new WebRequest(
236+
URI.create("http://localhost:8081/tenant-refresh/tenant-web-app-refresh/api/user")
237+
.toURL()));
238+
239+
Cookie sessionCookie2 = getSessionCookie(webClient, "tenant-web-app-refresh");
240+
assertNotNull(sessionCookie2);
241+
assertNotEquals(sessionCookie2.getValue(), sessionCookie.getValue());
242+
JsonObject jwtHeaders2 = getIdTokenHeaders(sessionCookie2.getValue());
243+
assertTrue(jwtHeaders2.getBoolean("internal"));
244+
245+
atSessionCookies = getSessionAtCookie(page.getWebClient(), "tenant-web-app-refresh");
246+
assertEquals(3, atSessionCookies.size());
247+
Cookie rtCookie2 = getSessionRtCookie(webClient, "tenant-web-app-refresh");
248+
assertNotNull(rtCookie2);
249+
assertEquals(rtCookie2.getValue(), rtCookie.getValue());
250+
251+
assertEquals("userName: alice, idToken: true, accessToken: true, accessTokenLongStringClaim: "
252+
+ getAccessTokenLongStringClaim(atSessionCookies)
253+
+ ", refreshToken: true",
254+
webResponse.getContentAsString());
255+
256+
webClient.getCookieManager().clearCookies();
257+
}
258+
}
259+
260+
private static JsonObject getIdTokenHeaders(String value) throws Exception {
261+
return OidcUtils.decodeJwtHeaders(value);
262+
}
263+
264+
@Test
265+
public void testCodeFlowRefreshTokensWhileIdTokenIsValid() throws Exception {
266+
try (final WebClient webClient = createWebClient()) {
267+
HtmlPage page = webClient.getPage("http://localhost:8081/tenant-refresh/tenant-web-app-refresh/api/user");
268+
assertEquals("Sign in to quarkus-webapp", page.getTitleText());
269+
HtmlForm loginForm = page.getForms().get(0);
270+
loginForm.getInputByName("username").setValueAttribute("alice");
271+
loginForm.getInputByName("password").setValueAttribute("alice");
272+
page = loginForm.getButtonByName("login").click();
273+
274+
Cookie sessionCookie = getSessionCookie(page.getWebClient(), "tenant-web-app-refresh");
275+
assertNotNull(sessionCookie);
276+
JsonObject jwtHeaders = getIdTokenHeaders(sessionCookie.getValue());
277+
assertFalse(jwtHeaders.getBoolean("internal", false));
278+
279+
Set<Cookie> atSessionCookies = getSessionAtCookie(page.getWebClient(), "tenant-web-app-refresh");
280+
assertEquals(3, atSessionCookies.size());
281+
282+
Cookie rtCookie = getSessionRtCookie(page.getWebClient(), "tenant-web-app-refresh");
283+
assertNotNull(rtCookie);
284+
285+
assertEquals("userName: alice, idToken: true, accessToken: true, accessTokenLongStringClaim: "
286+
+ getAccessTokenLongStringClaim(atSessionCookies)
287+
+ ", refreshToken: true",
288+
page.getBody().asNormalizedText());
289+
290+
// Wait till a valid ID token is within the refresh token skew
232291
Thread.sleep(2 * 1000);
233292

234293
webClient.getOptions().setRedirectEnabled(false);
@@ -240,6 +299,10 @@ public void testCodeFlowRefreshTokens() throws IOException, InterruptedException
240299
Cookie sessionCookie2 = getSessionCookie(webClient, "tenant-web-app-refresh");
241300
assertNotNull(sessionCookie2);
242301
assertEquals(sessionCookie2.getValue(), sessionCookie.getValue());
302+
303+
JsonObject jwtHeaders2 = getIdTokenHeaders(sessionCookie2.getValue());
304+
assertFalse(jwtHeaders2.getBoolean("internal", false));
305+
243306
atSessionCookies = getSessionAtCookie(page.getWebClient(), "tenant-web-app-refresh");
244307
assertEquals(3, atSessionCookies.size());
245308
Cookie rtCookie2 = getSessionRtCookie(webClient, "tenant-web-app-refresh");

integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
405405
// be returned to Quarkus, analyzed and refreshed
406406
assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 299 + 300 + 3);
407407

408+
assertEquals(299, decryptAccessTokenExpiryTime(webClient, "code-flow-user-info-github-cached-in-idtoken"));
409+
408410
// This is the initial call to the token endpoint where the code was exchanged for tokens
409411
wireMockServer.verify(1,
410412
postRequestedFor(urlPathMatching("/auth/realms/quarkus/access_token_refreshed")));
@@ -421,12 +423,14 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
421423

422424
issuedAt = idTokenClaims.getLong("iat");
423425
expiresAt = idTokenClaims.getLong("exp");
424-
assertEquals(305, expiresAt - issuedAt);
426+
assertEquals(299, expiresAt - issuedAt);
425427

426428
sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken");
427429
date = sessionCookie.getExpires();
428-
assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 305 + 300);
429-
assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 305 + 300 + 3);
430+
assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 299 + 300);
431+
assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 299 + 300 + 3);
432+
433+
assertEquals(305, decryptAccessTokenExpiryTime(webClient, "code-flow-user-info-github-cached-in-idtoken"));
430434

431435
// access token must've been refreshed
432436
wireMockServer.verify(1,
@@ -699,11 +703,33 @@ private void doTestCodeFlowUserInfoDynamicGithubUpdate() throws Exception {
699703
}
700704
}
701705

702-
private JsonObject decryptIdToken(WebClient webClient, String tenantId) throws Exception {
706+
private static JsonObject decryptIdToken(WebClient webClient, String tenantId) throws Exception {
707+
Cookie sessionCookie = getSessionCookie(webClient, tenantId);
708+
assertNotNull(sessionCookie);
709+
710+
SecretKey key = getSessionCookieDecryptionKey(webClient, tenantId);
711+
712+
String decryptedSessionCookie = OidcUtils.decryptString(sessionCookie.getValue(), key);
713+
714+
String encodedIdToken = decryptedSessionCookie.split("\\|")[0];
715+
716+
return OidcCommonUtils.decodeJwtContent(encodedIdToken);
717+
}
718+
719+
private static int decryptAccessTokenExpiryTime(WebClient webClient, String tenantId) throws Exception {
703720
Cookie sessionCookie = getSessionCookie(webClient, tenantId);
704721
assertNotNull(sessionCookie);
705722

706-
SecretKey key = null;
723+
SecretKey key = getSessionCookieDecryptionKey(webClient, tenantId);
724+
725+
String decryptedSessionCookie = OidcUtils.decryptString(sessionCookie.getValue(), key);
726+
727+
// idtoken|accesstoken|accesstoken-exp-in-time|...
728+
return Integer.valueOf(decryptedSessionCookie.split("\\|")[2]);
729+
730+
}
731+
732+
private static SecretKey getSessionCookieDecryptionKey(WebClient webClient, String tenantId) throws Exception {
707733
if ("code-flow-user-info-github".equals(tenantId)) {
708734
PrivateKey privateKey = KeyUtils.tryAsPemSigningPrivateKey(
709735
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyXwKqKL/"
@@ -724,18 +750,12 @@ private JsonObject decryptIdToken(WebClient webClient, String tenantId) throws E
724750
+ "r/ATkc4sG4kxQKBgBL9neT0TmJtxlYGzjNcjdJXs3Q91+nZt3DRMGT9s0917SuP77+FdJYocDiH1rVa9sGG8rkh1jTdqliAxDXwIm5I"
725751
+ "GS/0OBnkaN1nnGDk5yTiYxOutC5NSj7ecI5Erud8swW6iGqgz2ioFpGxxIYqRlgTv/6mVt41KALfKrYIkVLw",
726752
SignatureAlgorithm.RS256);
727-
key = OidcUtils.createSecretKeyFromDigest(privateKey.getEncoded());
753+
return OidcUtils.createSecretKeyFromDigest(privateKey.getEncoded());
728754
} else {
729-
key = OidcUtils.createSecretKeyFromDigest(
755+
return OidcUtils.createSecretKeyFromDigest(
730756
"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
731757
.getBytes(StandardCharsets.UTF_8));
732758
}
733-
734-
String decryptedSessionCookie = OidcUtils.decryptString(sessionCookie.getValue(), key);
735-
736-
String encodedIdToken = decryptedSessionCookie.split("\\|")[0];
737-
738-
return OidcCommonUtils.decodeJwtContent(encodedIdToken);
739759
}
740760

741761
private WebClient createWebClient() {
@@ -974,11 +994,11 @@ private void defineCodeFlowLogoutStub() {
974994
.withTransformers("response-template")));
975995
}
976996

977-
private Cookie getSessionCookie(WebClient webClient, String tenantId) {
997+
private static Cookie getSessionCookie(WebClient webClient, String tenantId) {
978998
return webClient.getCookieManager().getCookie("q_session" + (tenantId == null ? "" : "_" + tenantId));
979999
}
9801000

981-
private Cookie getStateCookie(WebClient webClient, String tenantId) {
1001+
private static Cookie getStateCookie(WebClient webClient, String tenantId) {
9821002
return webClient.getCookieManager().getCookies().stream()
9831003
.filter(c -> c.getName().startsWith("q_auth" + (tenantId == null ? "" : "_" + tenantId))).findFirst()
9841004
.orElse(null);

0 commit comments

Comments
 (0)