keycloak-memoizeit

Changes

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java
index 420ae93..a58a05a 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java
@@ -35,7 +35,8 @@ public class OIDCAuthenticationError implements AuthenticationError {
         CODE_TO_TOKEN_FAILURE,
         INVALID_TOKEN,
         STALE_TOKEN,
-        NO_AUTHORIZATION_HEADER
+        NO_AUTHORIZATION_HEADER,
+        NO_QUERY_PARAMETER_ACCESS_TOKEN
     }
 
     private Reason reason;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParamterTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParamterTokenRequestAuthenticator.java
new file mode 100644
index 0000000..5ee6662
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/QueryParamterTokenRequestAuthenticator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters;
+
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.spi.AuthOutcome;
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:froehlich.ch@gmail.com">Christian Froehlich</a>
+ * @version $Revision: 1 $
+ */
+public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator {
+    public static final String ACCESS_TOKEN = "access_token";
+    protected Logger log = Logger.getLogger(QueryParamterTokenRequestAuthenticator.class);
+
+    public QueryParamterTokenRequestAuthenticator(KeycloakDeployment deployment) {
+        super(deployment);
+    }
+
+    public AuthOutcome authenticate(HttpFacade exchange) {
+        tokenString = null;
+        tokenString = getAccessTokenFromQueryParamter(exchange);
+        if (tokenString == null || tokenString.trim().isEmpty()) {
+            challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_QUERY_PARAMETER_ACCESS_TOKEN, null, null);
+            return AuthOutcome.NOT_ATTEMPTED;
+        }
+        return (authenticateToken(exchange, tokenString));
+    }
+
+    String getAccessTokenFromQueryParamter(HttpFacade exchange) {
+        try {
+            if (exchange != null && exchange.getRequest() != null) {
+                return exchange.getRequest().getQueryParamValue(ACCESS_TOKEN);
+            }
+        } catch (Exception ignore) {
+        }
+        return null;
+    }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
index 2cd1261..c04f21c 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
@@ -61,7 +61,7 @@ public abstract class RequestAuthenticator {
         if (log.isTraceEnabled()) {
             log.trace("try bearer");
         }
-        
+
         AuthOutcome outcome = bearer.authenticate(facade);
         if (outcome == AuthOutcome.FAILED) {
             challenge = bearer.getChallenge();
@@ -74,18 +74,36 @@ public abstract class RequestAuthenticator {
             return AuthOutcome.AUTHENTICATED;
         }
 
+        QueryParamterTokenRequestAuthenticator queryParamAuth = createQueryParamterTokenRequestAuthenticator();
+        if (log.isTraceEnabled()) {
+            log.trace("try query paramter auth");
+        }
+
+        outcome = queryParamAuth.authenticate(facade);
+        if (outcome == AuthOutcome.FAILED) {
+            challenge = queryParamAuth.getChallenge();
+            log.debug("QueryParamAuth auth FAILED");
+            return AuthOutcome.FAILED;
+        } else if (outcome == AuthOutcome.AUTHENTICATED) {
+            if (verifySSL()) return AuthOutcome.FAILED;
+            log.debug("QueryParamAuth AUTHENTICATED");
+            completeAuthentication(queryParamAuth, "KEYCLOAK");
+            return AuthOutcome.AUTHENTICATED;
+        }
+
         if (deployment.isEnableBasicAuth()) {
             BasicAuthRequestAuthenticator basicAuth = createBasicAuthAuthenticator();
             if (log.isTraceEnabled()) {
                 log.trace("try basic auth");
             }
-    
+
             outcome = basicAuth.authenticate(facade);
             if (outcome == AuthOutcome.FAILED) {
                 challenge = basicAuth.getChallenge();
                 log.debug("BasicAuth FAILED");
                 return AuthOutcome.FAILED;
             } else if (outcome == AuthOutcome.AUTHENTICATED) {
+                if (verifySSL()) return AuthOutcome.FAILED;
                 log.debug("BasicAuth AUTHENTICATED");
                 completeAuthentication(basicAuth, "BASIC");
                 return AuthOutcome.AUTHENTICATED;
@@ -150,6 +168,10 @@ public abstract class RequestAuthenticator {
         return new BasicAuthRequestAuthenticator(deployment);
     }
 
+    protected QueryParamterTokenRequestAuthenticator createQueryParamterTokenRequestAuthenticator() {
+        return new QueryParamterTokenRequestAuthenticator(deployment);
+    }
+
     protected void completeAuthentication(OAuthRequestAuthenticator oauth) {
         RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, tokenStore, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken());
         final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(AdapterUtils.getPrincipalName(deployment, oauth.getToken()), session);
@@ -158,10 +180,12 @@ public abstract class RequestAuthenticator {
     }
 
     protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
+
     protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method);
 
     /**
      * After code is received, we change the session id if possible to guard against https://www.owasp.org/index.php/Session_Fixation
+     *
      * @param create
      * @return
      */
diff --git a/adapters/oidc/spring-boot/pom.xml b/adapters/oidc/spring-boot/pom.xml
index a60441d..1d24a54 100755
--- a/adapters/oidc/spring-boot/pom.xml
+++ b/adapters/oidc/spring-boot/pom.xml
@@ -89,6 +89,24 @@
     </dependency>
 
     <dependency>
+      <groupId>io.undertow</groupId>
+      <artifactId>undertow-servlet</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>io.undertow</groupId>
+      <artifactId>undertow-core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.keycloak</groupId>
+      <artifactId>keycloak-undertow-adapter-spi</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>test</scope>
diff --git a/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java b/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java
index 68c750c..5c1b546 100755
--- a/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java
+++ b/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.adapters.springboot;
 
+import io.undertow.servlet.api.DeploymentInfo;
+import io.undertow.servlet.api.WebResourceCollection;
 import org.apache.catalina.Context;
 import org.apache.tomcat.util.descriptor.web.LoginConfig;
 import org.apache.tomcat.util.descriptor.web.SecurityCollection;
@@ -28,6 +30,7 @@ import org.eclipse.jetty.util.security.Constraint;
 import org.eclipse.jetty.webapp.WebAppContext;
 import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator;
 import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
+import org.keycloak.adapters.undertow.KeycloakServletExtension;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
@@ -108,7 +111,53 @@ public class KeycloakSpringBootConfiguration {
     @Bean
     @ConditionalOnClass(name = {"io.undertow.Undertow"})
     public UndertowDeploymentInfoCustomizer undertowKeycloakContextCustomizer() {
-        throw new IllegalArgumentException("Undertow Keycloak integration is not yet implemented");
+        return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
+    }
+
+    static class KeycloakUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
+
+        private final KeycloakSpringBootProperties keycloakProperties;
+
+        public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
+            this.keycloakProperties = keycloakProperties;
+        }
+
+        @Override
+        public void customize(DeploymentInfo deploymentInfo) {
+
+            io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm());
+            loginConfig.addFirstAuthMethod("KEYCLOAK");
+
+            deploymentInfo.setLoginConfig(loginConfig);
+
+            deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
+            deploymentInfo.addSecurityConstraints(getSecurityConstraints());
+
+            deploymentInfo.addServletExtension(new KeycloakServletExtension());
+        }
+
+        private List<io.undertow.servlet.api.SecurityConstraint> getSecurityConstraints() {
+
+            List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
+            for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
+
+                for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
+
+                    io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
+                    undertowSecurityConstraint.addRolesAllowed(collectionDefinition.getAuthRoles());
+
+                    WebResourceCollection webResourceCollection = new WebResourceCollection();
+                    webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
+                    webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods());
+                    webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns());
+
+                    undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
+
+                    undertowSecurityConstraints.add(undertowSecurityConstraint);
+                }
+            }
+            return undertowSecurityConstraints;
+        }
     }
 
     static class KeycloakJettyServerCustomizer implements JettyServerCustomizer {
diff --git a/examples/basic-auth/README.md b/examples/basic-auth/README.md
index be96c59..8eb4fc5 100644
--- a/examples/basic-auth/README.md
+++ b/examples/basic-auth/README.md
@@ -22,7 +22,7 @@ Step 2: Deploy and run the example
 
     curl http://admin:password@localhost:8080/basicauth/service/echo?value=hello
 
-(If we navigate directly to http://localhost:8080/basicauth/service/echo?value=hello, we get "Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client.").
+(If we navigate directly to http://localhost:8080/basicauth/service/echo?value=hello, we get an error in the browser because the request is not authenticated).
 
 This should result in the value 'hello' being returned as a response.
 
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 3f8d32a..85fea77 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
@@ -187,7 +187,7 @@ public class LogoutEndpoint {
             throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
         }
         try {
-            RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken);
+            RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken, false);
             UserSessionModel userSessionModel = session.sessions().getUserSession(realm, token.getSessionState());
             if (userSessionModel != null) {
                 logout(userSessionModel);
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 54bf9ae..4fedd83 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -244,16 +244,23 @@ public class TokenManager {
     }
 
     public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
+        return verifyRefreshToken(realm, encodedRefreshToken, true);
+    }
+
+    public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException {
         try {
             RefreshToken refreshToken = toRefreshToken(realm, encodedRefreshToken);
 
-            if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
-                throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
-            }
+            if (checkExpiration) {
+                if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
+                    throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
+                }
 
-            if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
-                throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
+                if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
+                    throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
+                }
             }
+
             return refreshToken;
         } catch (JWSInputException e) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 15b48fa..a602ffd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -203,4 +203,17 @@ public class AdapterTest {
         testStrategy.testAccountManagementSessionsLogout();
     }
 
+    /**
+     * KEYCLOAK-1733
+     */
+    @Test
+    public void testNullQueryParameterAccessToken() throws Exception {
+        testStrategy.testNullQueryParameterAccessToken();
+    }
+
+    @Test
+    public void testRestCallWithAccessTokenAsQueryParameter() throws Exception {
+        testStrategy.testRestCallWithAccessTokenAsQueryParameter();
+
+    }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index c5790dc..507c0fe 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -17,34 +17,29 @@
 package org.keycloak.testsuite.adapter;
 
 import org.apache.http.conn.params.ConnManagerParams;
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.junit.Assert;
 import org.junit.rules.ExternalResource;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.adapters.OIDCAuthenticationError;
-import org.keycloak.common.Version;
-import org.keycloak.representations.VersionRepresentation;
 import org.keycloak.admin.client.Keycloak;
+import org.keycloak.common.Version;
+import org.keycloak.common.util.Time;
 import org.keycloak.constants.AdapterConstants;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
+import org.keycloak.models.*;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.VersionRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.testsuite.KeycloakServer;
 import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.pages.AccountSessionsPage;
 import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.rule.AbstractKeycloakRule;
-import org.keycloak.testsuite.rule.ErrorServlet;
-import org.keycloak.testsuite.rule.KeycloakRule;
-import org.keycloak.testsuite.rule.WebResource;
-import org.keycloak.testsuite.rule.WebRule;
-import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.testsuite.rule.*;
 import org.keycloak.util.BasicAuthHelper;
-import org.keycloak.common.util.Time;
 import org.openqa.selenium.WebDriver;
 
 import javax.ws.rs.client.Client;
@@ -59,7 +54,6 @@ import java.net.URI;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
-import org.keycloak.representations.idm.UserRepresentation;
 
 /**
  * Tests Undertow Adapter
@@ -144,7 +138,8 @@ public class AdapterTestStrategy extends ExternalResource {
         System.out.println("insecure: ");
         System.out.println(driver.getPageSource());
         Assert.assertTrue(driver.getPageSource().contains("Insecure Page"));
-        if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
+        if (System.getProperty("insecure.user.principal.unsupported") == null)
+            Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
 
         // test logout
 
@@ -385,6 +380,26 @@ public class AdapterTestStrategy extends ExternalResource {
     }
 
     /**
+     * KEYCLOAK-1733
+     *
+     * @throws Exception
+     */
+    public void testNullQueryParameterAccessToken() throws Exception {
+        Client client = ClientBuilder.newClient();
+        WebTarget target = client.target(APP_SERVER_BASE_URL + "/customer-db/");
+        Response response = target.request().get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        target = client.target(APP_SERVER_BASE_URL + "/customer-db?access_token=");
+        response = target.request().get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        client.close();
+    }
+
+    /**
      * KEYCLOAK-1368
      * @throws Exception
      */
@@ -406,7 +421,7 @@ public class AdapterTestStrategy extends ExternalResource {
         Assert.assertTrue(errorPageResponse.contains("Error Page"));
         response.close();
         Assert.assertNotNull(ErrorServlet.authError);
-        OIDCAuthenticationError error = (OIDCAuthenticationError)ErrorServlet.authError;
+        OIDCAuthenticationError error = (OIDCAuthenticationError) ErrorServlet.authError;
         Assert.assertEquals(OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, error.getReason());
 
         ErrorServlet.authError = null;
@@ -422,7 +437,7 @@ public class AdapterTestStrategy extends ExternalResource {
         Assert.assertTrue(errorPageResponse.contains("Error Page"));
         response.close();
         Assert.assertNotNull(ErrorServlet.authError);
-        error = (OIDCAuthenticationError)ErrorServlet.authError;
+        error = (OIDCAuthenticationError) ErrorServlet.authError;
         Assert.assertEquals(OIDCAuthenticationError.Reason.INVALID_TOKEN, error.getReason());
 
         client.close();
@@ -464,8 +479,8 @@ public class AdapterTestStrategy extends ExternalResource {
         String header = BasicAuthHelper.createHeader("customer-portal", "password");
         Form form = new Form();
         form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)
-            .param("username", "monkey@redhat.com")
-            .param("password", "password");
+                .param("username", "monkey@redhat.com")
+                .param("password", "password");
         Response response = target.request()
                 .header(HttpHeaders.AUTHORIZATION, header)
                 .post(Entity.form(form));
@@ -496,7 +511,6 @@ public class AdapterTestStrategy extends ExternalResource {
     }
 
 
-
     public void testAuthenticated() throws Exception {
         // test login to customer-portal which does a bearer request to customer-db
         driver.navigate().to(APP_SERVER_BASE_URL + "/secure-portal");
@@ -522,6 +536,53 @@ public class AdapterTestStrategy extends ExternalResource {
     }
 
     /**
+     * KEYCLOAK-1733
+     *
+     * @throws Exception
+     */
+    public void testRestCallWithAccessTokenAsQueryParameter() throws Exception {
+        String accessToken = getAccessToken();
+        Client client = ClientBuilder.newClient();
+        try {
+            // test without token
+            Response response = client.target(APP_SERVER_BASE_URL + "/customer-db").request().get();
+            Assert.assertEquals(401, response.getStatus());
+            response.close();
+            // test with access_token as QueryParamter
+            response = client.target(APP_SERVER_BASE_URL + "/customer-db").queryParam("access_token", accessToken).request().get();
+            Assert.assertEquals(200, response.getStatus());
+            response.close();
+        } finally {
+            client.close();
+        }
+    }
+
+    private String getAccessToken() throws JSONException {
+        String tokenUrl = AUTH_SERVER_URL + "/realms/demo/protocol/openid-connect/token";
+
+        Client client = ClientBuilder.newClient();
+        try {
+            WebTarget webTarget = client.target(tokenUrl);
+
+            Form form = new Form();
+            form.param("grant_type", "password");
+            form.param("client_id", "customer-portal-public");
+            form.param("username", "bburke@redhat.com");
+            form.param("password", "password");
+            Response response = webTarget.request().post(Entity.form(form));
+
+            Assert.assertEquals(200, response.getStatus());
+
+            JSONObject jsonObject = new JSONObject(response.readEntity(String.class));
+            System.out.println(jsonObject);
+            response.close();
+            return jsonObject.getString("access_token");
+        } finally {
+            client.close();
+        }
+    }
+
+    /**
      * KEYCLOAK-732
      *
      * @throws Throwable
diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
index 70dc85a..aaac871 100755
--- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
@@ -157,6 +157,12 @@
             ]
         },
         {
+            "name": "customer-portal-public",
+            "enabled": true,
+            "publicClient": true,
+            "directAccessGrantsEnabled": true
+        },
+        {
             "name": "product-portal",
             "enabled": true,
             "adminUrl": "http://localhost:8081/product-portal",
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.bat b/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.bat
new file mode 100644
index 0000000..d3a5cc4
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.bat
@@ -0,0 +1,7 @@
+set NOPAUSE=true
+
+call %JBOSS_HOME%\bin\jboss-cli.bat --command="patch apply %PATCH_ZIP%"
+
+if %ERRORLEVEL% neq 0 set ERROR=%ERRORLEVEL%
+exit /b %ERROR%
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.sh b/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.sh
new file mode 100755
index 0000000..4a44294
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/install-patch.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+echo "JBOSS_HOME=$JBOSS_HOME"
+
+if [ ! -d "$JBOSS_HOME/bin" ] ; then
+    >&2 echo "JBOSS_HOME/bin doesn't exist"
+    exit 1
+fi
+
+cd $JBOSS_HOME/bin
+
+RESULT=0
+./jboss-cli.sh --command="patch apply $PATCH_ZIP"
+if [ $? -ne 0 ]; then RESULT=1; fi
+   exit $RESULT
+fi
+
+exit 1
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index bf6b3e0..00c9fd3 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -293,6 +293,10 @@
                         </executions>
                     </plugin>
                     <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>exec-maven-plugin</artifactId>
+                    </plugin>
+                    <plugin>
                         <artifactId>maven-assembly-plugin</artifactId>
                         <executions>
                             <execution>
@@ -587,7 +591,42 @@
                 </pluginManagement>
             </build>
         </profile>
-
+        <profile>
+            <id>auth-server-apply-patch</id>
+            <activation>
+                <property>
+                    <name>auth.server.patch.zip</name>
+                </property>
+            </activation>
+            <build>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <groupId>org.codehaus.mojo</groupId>
+                            <artifactId>exec-maven-plugin</artifactId>
+                            <executions>
+                                <execution>
+                                    <id>install-patch</id>
+                                    <phase>process-resources</phase>
+                                    <goals>
+                                        <goal>exec</goal>
+                                    </goals>
+                                </execution>
+                            </executions>
+                            <configuration>
+                                <executable>${common.resources}/install-patch.${script.suffix}</executable>
+                                <workingDirectory>${auth.server.home}/bin</workingDirectory>
+                                <environmentVariables>
+                                    <JAVA_HOME>${auth.server.java.home}</JAVA_HOME>
+                                    <JBOSS_HOME>${auth.server.home}</JBOSS_HOME>
+                                    <PATCH_ZIP>${auth.server.patch.zip}</PATCH_ZIP>
+                                </environmentVariables>
+                            </configuration>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
+        </profile>
         <profile>
             <id>auth-server-cluster</id>
             <properties>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 23c365f..cef65dc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -253,7 +253,7 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
     @Test
     public void badClientSalesPostSigTest() {
         badClientSalesPostSigServletPage.navigateTo();
-        waitUntilElement(By.xpath("//body")).text().contains("invalidRequesterMessage");
+        waitUntilElement(By.xpath("//body")).text().contains("Invalid requester");
     }
 
     @Test
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
new file mode 100644
index 0000000..38dde74
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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 java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LogoutTest extends AbstractKeycloakTest {
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Override
+    public void beforeAbstractKeycloakTest() throws Exception {
+        super.beforeAbstractKeycloakTest();
+    }
+
+    @Before
+    public void clientConfiguration() {
+        ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+        RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener();
+
+        testRealms.add(realm.build());
+    }
+
+    @Test
+    public void postLogout() 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 refreshTokenString = tokenResponse.getRefreshToken();
+
+        HttpResponse response = oauth.doLogout(refreshTokenString, "password");
+        assertEquals(204, response.getStatusLine().getStatusCode());
+
+        assertNotNull(testingClient.testApp().getAdminLogoutAction());
+    }
+
+    @Test
+    public void postLogoutExpiredRefreshToken() 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 refreshTokenString = tokenResponse.getRefreshToken();
+
+        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());
+
+        assertNotNull(testingClient.testApp().getAdminLogoutAction());
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
index c9746d2..3653065 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
@@ -132,6 +132,11 @@ public class RealmBuilder {
         return this;
     }
 
+    public RealmBuilder notBefore(int i) {
+        rep.setNotBefore(i);
+        return this;
+    }
+
     public RealmBuilder otpLookAheadWindow(int i) {
         rep.setOtpPolicyLookAheadWindow(i);
         return this;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index 5427075..11e25d3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -105,7 +105,7 @@
       "redirectUris": [
         "http://localhost:8180/auth/realms/master/app/auth/*"
       ],
-      "adminUrl": "http://localhost:8180/auth/realms/master/app/logout",
+      "adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
       "secret": "password"
     },
     {