keycloak-memoizeit
Changes
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 51(+40 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java 30(+27 -3)
Details
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 94aedc4..fd50025 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -113,18 +113,11 @@ public class LogoutEndpoint {
}
UserSessionModel userSession = null;
- boolean error = false;
if (encodedIdToken != null) {
try {
- IDToken idToken = tokenManager.verifyIDToken(session, realm, encodedIdToken);
+ IDToken idToken = tokenManager.verifyIDTokenSignature(session, realm, encodedIdToken);
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
- if (userSession == null) {
- error = true;
- }
} catch (OAuthErrorException e) {
- error = true;
- }
- if (error) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 43985f2..6072af9 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -316,6 +316,21 @@ public class TokenManager {
}
}
+ public IDToken verifyIDTokenSignature(KeycloakSession session, RealmModel realm, String encodedIDToken) throws OAuthErrorException {
+ try {
+ JWSInput jws = new JWSInput(encodedIDToken);
+ IDToken idToken;
+ if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
+ }
+ idToken = jws.readJsonContent(IDToken.class);
+
+ return idToken;
+ } catch (JWSInputException e) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
+ }
+ }
+
public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
AccessToken token = initToken(realm, client, user, userSession, clientSession, session.getContext().getUri());
for (RoleModel role : requestedRoles) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index b8ed52d..d7509ed 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -111,6 +111,42 @@ public class OAuthClient {
private Map<String, PublicKey> publicKeys = new HashMap<>();
+ public class LogoutUrlBuilder {
+ private final UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
+
+ public LogoutUrlBuilder idTokenHint(String idTokenHint) {
+ if (idTokenHint != null) {
+ b.queryParam("id_token_hint", idTokenHint);
+ }
+ return this;
+ }
+
+ public LogoutUrlBuilder postLogoutRedirectUri(String redirectUri) {
+ if (redirectUri != null) {
+ b.queryParam("post_logout_redirect_uri", redirectUri);
+ }
+ return this;
+ }
+
+ public LogoutUrlBuilder redirectUri(String redirectUri) {
+ if (redirectUri != null) {
+ b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+ }
+ return this;
+ }
+
+ public LogoutUrlBuilder sessionState(String sessionState) {
+ if (sessionState != null) {
+ b.queryParam("session_state", sessionState);
+ }
+ return this;
+ }
+
+ public String build() {
+ return b.build(realm).toString();
+ }
+ }
+
public void init(Keycloak adminClient, WebDriver driver) {
this.adminClient = adminClient;
this.driver = driver;
@@ -341,10 +377,10 @@ public class OAuthClient {
}
- public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
+ public CloseableHttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
CloseableHttpClient client = new DefaultHttpClient();
try {
- HttpPost post = new HttpPost(getLogoutUrl(null, null));
+ HttpPost post = new HttpPost(getLogoutUrl().build());
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
if (refreshToken != null) {
@@ -558,15 +594,8 @@ public class OAuthClient {
return b.build(realm).toString();
}
- public String getLogoutUrl(String redirectUri, String sessionState) {
- UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
- if (redirectUri != null) {
- b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
- }
- if (sessionState != null) {
- b.queryParam("session_state", sessionState);
- }
- return b.build(realm).toString();
+ public LogoutUrlBuilder getLogoutUrl() {
+ return new LogoutUrlBuilder();
}
public String getResourceOwnerPasswordCredentialGrantUrl() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
index d0ea630..9b83583 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -66,7 +66,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
String redirectUri = AppPage.baseUrl + "?logout";
- String logoutUrl = oauth.getLogoutUrl(redirectUri, null);
+ String logoutUrl = oauth.getLogoutUrl().redirectUri(redirectUri).build();
driver.navigate().to(logoutUrl);
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
@@ -89,7 +89,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
String sessionId = events.expectLogin().assertEvent().getSessionId();
- String logoutUrl = oauth.getLogoutUrl(null, sessionId);
+ String logoutUrl = oauth.getLogoutUrl().sessionState(sessionId).build();
driver.navigate().to(logoutUrl);
events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
@@ -118,7 +118,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().session(sessionId).removeDetail(Details.USERNAME).assertEvent();
// Logout session 1 by redirect
- driver.navigate().to(oauth.getLogoutUrl(AppPage.baseUrl, null));
+ driver.navigate().to(oauth.getLogoutUrl().redirectUri(AppPage.baseUrl).build());
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AppPage.baseUrl).assertEvent();
// Check session 1 not logged-in
@@ -176,4 +176,28 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
rep.setRememberMe(enabled);
adminClient.realm("test").update(rep);
}
+
+ @Test
+ public void logoutSessionWhenLoggedOutByAdmin() {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+ adminClient.realm("test").logoutAll();
+
+ String logoutUrl = oauth.getLogoutUrl().sessionState(sessionId).build();
+ driver.navigate().to(logoutUrl);
+
+ assertEquals(logoutUrl, driver.getCurrentUrl());
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 0e78d2e..998cc87 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -755,7 +755,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
- return sendRequest(oauth.getLogoutUrl(null, null), parameters);
+ return sendRequest(oauth.getLogoutUrl().build(), parameters);
}
private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
index 38dde74..e7e72c0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
@@ -17,23 +17,28 @@
package org.keycloak.testsuite.oauth;
-import org.apache.http.HttpResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.util.ClientManager;
-import org.keycloak.testsuite.util.OAuthClient;
-import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.util.*;
import java.util.List;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response.Status;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
@@ -72,10 +77,11 @@ public class LogoutTest extends AbstractKeycloakTest {
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String refreshTokenString = tokenResponse.getRefreshToken();
- HttpResponse response = oauth.doLogout(refreshTokenString, "password");
- assertEquals(204, response.getStatusLine().getStatusCode());
+ try (CloseableHttpResponse response = oauth.doLogout(refreshTokenString, "password")) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.NO_CONTENT));
- assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ }
}
@Test
@@ -91,10 +97,83 @@ public class LogoutTest extends AbstractKeycloakTest {
adminClient.realm("test").update(RealmBuilder.create().notBefore(Time.currentTime() + 1).build());
// Logout should succeed with expired refresh token, see KEYCLOAK-3302
- HttpResponse response = oauth.doLogout(refreshTokenString, "password");
- assertEquals(204, response.getStatusLine().getStatusCode());
+ try (CloseableHttpResponse response = oauth.doLogout(refreshTokenString, "password")) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.NO_CONTENT));
+
+ assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ }
+ }
+
+ @Test
+ public void postLogoutWithValidIdToken() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ oauth.clientSessionState("client-session");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String idTokenString = tokenResponse.getIdToken();
+
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(idTokenString)
+ .postLogoutRedirectUri(AppPage.baseUrl)
+ .build();
+
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
+ assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(AppPage.baseUrl));
+ }
+ }
+
+ @Test
+ public void postLogoutWithExpiredIdToken() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ oauth.clientSessionState("client-session");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String idTokenString = tokenResponse.getIdToken();
+
+ // Logout should succeed with expired ID token, see KEYCLOAK-3399
+ setTimeOffset(60 * 60 * 24);
+
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(idTokenString)
+ .postLogoutRedirectUri(AppPage.baseUrl)
+ .build();
+
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
+ assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(AppPage.baseUrl));
+ }
+ }
+
+ @Test
+ public void postLogoutWithValidIdTokenWhenLoggedOutByAdmin() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ oauth.clientSessionState("client-session");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String idTokenString = tokenResponse.getIdToken();
+
+ adminClient.realm("test").logoutAll();
+
+ // Logout should succeed with user already logged out, see KEYCLOAK-3399
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(idTokenString)
+ .postLogoutRedirectUri(AppPage.baseUrl)
+ .build();
- assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
+ assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(AppPage.baseUrl));
+ }
}
}