keycloak-memoizeit

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));
+        }
     }
 
 }