keycloak-aplcache

Merge pull request #3577 from mposolda/master KEYCLOAK-3823

12/1/2016 10:28:05 AM

Changes

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
index 63ea79e..0b33294 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
@@ -446,6 +446,16 @@ public class AdapterDeploymentContext {
         public int getMinTimeBetweenJwksRequests() {
             return delegate.getMinTimeBetweenJwksRequests();
         }
+
+        @Override
+        public int getPublicKeyCacheTtl() {
+            return delegate.getPublicKeyCacheTtl();
+        }
+
+        @Override
+        public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
+            delegate.setPublicKeyCacheTtl(publicKeyCacheTtl);
+        }
     }
 
     protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java
index 3e0f36d..b787796 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java
@@ -46,14 +46,7 @@ public class HttpAdapterUtils {
             }
             InputStream is = entity.getContent();
             try {
-                ByteArrayOutputStream os = new ByteArrayOutputStream();
-                int c;
-                while ((c = is.read()) != -1) {
-                    os.write(c);
-                }
-                byte[] bytes = os.toByteArray();
-                String json = new String(bytes);
-                return JsonSerialization.readValue(json, clazz);
+                return JsonSerialization.readValue(is, clazz);
             } finally {
                 try {
                     is.close();
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 1071b71..3f98a68 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -81,6 +81,7 @@ public class KeycloakDeployment {
     protected volatile int notBefore;
     protected int tokenMinimumTimeToLive;
     protected int minTimeBetweenJwksRequests;
+    protected int publicKeyCacheTtl;
     private PolicyEnforcer policyEnforcer;
 
     public KeycloakDeployment() {
@@ -384,6 +385,14 @@ public class KeycloakDeployment {
         this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests;
     }
 
+    public int getPublicKeyCacheTtl() {
+        return publicKeyCacheTtl;
+    }
+
+    public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
+        this.publicKeyCacheTtl = publicKeyCacheTtl;
+    }
+
     public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) {
         this.policyEnforcer = policyEnforcer;
     }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index f6c6f5e..85b19ca 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -105,6 +105,7 @@ public class KeycloakDeploymentBuilder {
         deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
         deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive());
         deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
+        deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
 
         if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
             throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
index 9305f32..45f420c 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
@@ -25,7 +25,6 @@ import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.common.util.Time;
 import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jwk.JWK;
-import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.util.JWKSUtils;
 
 import java.security.PublicKey;
@@ -48,15 +47,15 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
     @Override
     public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
         int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests();
+        int publicKeyCacheTtl = deployment.getPublicKeyCacheTtl();
+        int currentTime = Time.currentTime();
 
         // Check if key is in cache.
-        PublicKey publicKey = currentKeys.get(kid);
+        PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
         if (publicKey != null) {
             return publicKey;
         }
 
-        int currentTime = Time.currentTime();
-
         // Check if we are allowed to send request
         if (currentTime > lastRequestTime + minTimeBetweenRequests) {
             synchronized (this) {
@@ -70,11 +69,20 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
             }
         }
 
-        return currentKeys.get(kid);
+        return lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
 
     }
 
 
+    private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime, String kid) {
+        if (lastRequestTime + publicKeyCacheTtl > currentTime) {
+            return currentKeys.get(kid);
+        } else {
+            return null;
+        }
+    }
+
+
     private void sendRequest(KeycloakDeployment deployment) {
         if (log.isTraceEnabled()) {
             log.tracef("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
index 77eb475..233c1ed 100644
--- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
+++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
@@ -69,6 +69,7 @@ public class KeycloakDeploymentBuilderTest {
         assertEquals("email", deployment.getPrincipalAttribute());
         assertEquals(10, deployment.getTokenMinimumTimeToLive());
         assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
+        assertEquals(120, deployment.getPublicKeyCacheTtl());
     }
 
     @Test
@@ -78,6 +79,7 @@ public class KeycloakDeploymentBuilderTest {
 
         assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator);
         assertEquals(10, deployment.getMinTimeBetweenJwksRequests());
+        assertEquals(86400, deployment.getPublicKeyCacheTtl());
     }
 
     @Test
diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
index a8afd22..9f0a204 100644
--- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json
+++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json
@@ -30,5 +30,6 @@
     "token-store": "cookie",
     "principal-attribute": "email",
     "token-minimum-time-to-live": 10,
-    "min-time-between-jwks-requests": 20
+    "min-time-between-jwks-requests": 20,
+    "public-key-cache-ttl": 120
 }
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index 0ba327d..e4065bc 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -36,7 +36,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
         "client-keystore", "client-keystore-password", "client-key-password",
         "always-refresh-token",
         "register-node-at-startup", "register-node-period", "token-store", "principal-attribute",
-        "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests",
+        "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
+        "min-time-between-jwks-requests", "public-key-cache-ttl",
         "policy-enforcer"
 })
 public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig {
@@ -73,6 +74,8 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
     protected int tokenMinimumTimeToLive = 0;
     @JsonProperty("min-time-between-jwks-requests")
     protected int minTimeBetweenJwksRequests = 10;
+    @JsonProperty("public-key-cache-ttl")
+    protected int publicKeyCacheTtl = 86400; // 1 day
     @JsonProperty("policy-enforcer")
     protected PolicyEnforcerConfig policyEnforcerConfig;
 
@@ -233,4 +236,12 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
     public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) {
         this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests;
     }
+
+    public int getPublicKeyCacheTtl() {
+        return publicKeyCacheTtl;
+    }
+
+    public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
+        this.publicKeyCacheTtl = publicKeyCacheTtl;
+    }
 }
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
index 9d29973..bdbd4d9 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
@@ -50,8 +50,8 @@ import javax.servlet.ServletException;
 
 import java.lang.reflect.Field;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUndertowConfiguration> {
 
@@ -61,7 +61,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
     private KeycloakOnUndertowConfiguration configuration;
     private KeycloakSessionFactory sessionFactory;
 
-    Map<String, String> deployedArchivesToContextPath = new HashMap<>();
+    Map<String, String> deployedArchivesToContextPath = new ConcurrentHashMap<>();
 
     private DeploymentInfo createAuthServerDeploymentInfo() {
         ResteasyDeployment deployment = new ResteasyDeployment();
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java
index 1bfbb3e..3b3cc49 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java
@@ -23,12 +23,14 @@ import org.keycloak.adapters.AdapterDeploymentContext;
 import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
 import org.keycloak.common.util.Time;
+import org.keycloak.common.util.reflections.Reflections;
 
 import javax.servlet.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.reflect.Field;
 
 /**
  * Filter to handle "special" requests to perform actions on adapter side (for example setting time offset )
@@ -38,7 +40,7 @@ import java.io.PrintWriter;
 public class AdapterActionsFilter implements Filter {
 
     public static final String TIME_OFFSET_PARAM = "timeOffset";
-    public static final String RESET_PUBLIC_KEY_PARAM = "resetPublicKey";
+    public static final String RESET_DEPLOYMENT_PARAM = "resetDeployment";
 
     private static final Logger log = Logger.getLogger(AdapterActionsFilter.class);
 
@@ -54,19 +56,28 @@ public class AdapterActionsFilter implements Filter {
 
         //Accept timeOffset as argument to enforce timeouts
         String timeOffsetParam = request.getParameter(TIME_OFFSET_PARAM);
-        String resetPublicKey = request.getParameter(RESET_PUBLIC_KEY_PARAM);
+        String resetDeploymentParam = request.getParameter(RESET_DEPLOYMENT_PARAM);
 
         if (timeOffsetParam != null && !timeOffsetParam.isEmpty()) {
             int timeOffset = Integer.parseInt(timeOffsetParam);
             log.infof("Time offset updated to %d for application %s", timeOffset, servletReq.getRequestURI());
             Time.setOffset(timeOffset);
             writeResponse(servletResp, "Offset set successfully");
-        } else if (resetPublicKey != null && !resetPublicKey.isEmpty()) {
+        } else if (resetDeploymentParam != null && !resetDeploymentParam.isEmpty()) {
             AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getServletContext().getAttribute(AdapterDeploymentContext.class.getName());
-            KeycloakDeployment deployment = deploymentContext.resolveDeployment(null);
-            deployment.setPublicKeyLocator(new JWKPublicKeyLocator());
-            log.infof("Restarted publicKey locator for application %s", servletReq.getRequestURI());
-            writeResponse(servletResp, "PublicKeyLocator restarted successfully");
+
+            Field field = Reflections.findDeclaredField(AdapterDeploymentContext.class, "deployment");
+            Reflections.setAccessible(field);
+            KeycloakDeployment deployment = (KeycloakDeployment) Reflections.getFieldValue(field, deploymentContext);
+
+            Time.setOffset(0);
+            deployment.setNotBefore(0);
+            if (deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator) {
+                deployment.setPublicKeyLocator(new JWKPublicKeyLocator());
+            }
+
+            log.infof("Restarted PublicKeyLocator, notBefore and timeOffset for application %s", servletReq.getRequestURI());
+            writeResponse(servletResp, "Restarted PublicKeyLocator, notBefore and timeOffset successfully");
         } else {
             // Continue request
             chain.doFilter(request, response);
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java
index 3707172..6db5922 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/AbstractShowTokensServlet.java
@@ -49,6 +49,7 @@ public abstract class AbstractShowTokensServlet extends HttpServlet {
 
         return new StringBuilder("<span id=\"accessToken\">" + accessTokenPretty + "</span>")
                 .append("<span id=\"refreshToken\">" + refreshTokenPretty + "</span>")
+                .append("<span id=\"accessTokenString\">" + ctx.getTokenString() + "</span>")
                 .toString();
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java
index ea7ef6f..64d0de2 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java
@@ -38,6 +38,9 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl
     @FindBy(id = "refreshToken")
     private WebElement refreshToken;
 
+    @FindBy(id = "accessTokenString")
+    private WebElement accessTokenString;
+
 
     public AccessToken getAccessToken() {
         try {
@@ -51,13 +54,25 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl
         return null;
     }
 
+
     public RefreshToken getRefreshToken() {
         try {
             return JsonSerialization.readValue(refreshToken.getText(), RefreshToken.class);
         } catch (IOException e) {
             e.printStackTrace();
         } catch (NoSuchElementException nsee) {
-            log.warn("No idToken element found on the page");
+            log.warn("No refreshToken element found on the page");
+        }
+
+        return null;
+    }
+
+
+    public String getAccessTokenString() {
+        try {
+            return accessTokenString.getText();
+        } catch (NoSuchElementException nsee) {
+            log.warn("No accessTokenString element found on the page");
         }
 
         return null;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
index a40275f..cc018d2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java
@@ -111,14 +111,17 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
         testRealmPage.setAuthRealm(DEMO);
     }
 
-    protected void setAdapterAndServerTimeOffset(int timeOffset, String servletUri) {
+    protected void setAdapterAndServerTimeOffset(int timeOffset, String... servletUris) {
         setTimeOffset(timeOffset);
-        String timeOffsetUri = UriBuilder.fromUri(servletUri)
-                .queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset)
-                .build().toString();
 
-        driver.navigate().to(timeOffsetUri);
-        WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+        for (String servletUri : servletUris) {
+            String timeOffsetUri = UriBuilder.fromUri(servletUri)
+                    .queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset)
+                    .build().toString();
+
+            driver.navigate().to(timeOffsetUri);
+            WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
+        }
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
index 0ad81d5..a9ce39a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
@@ -27,10 +27,4 @@ public abstract class AbstractDemoFilterServletAdapterTest extends AbstractDemoS
 
     }
 
-    @Test
-    @Override
-    @Ignore
-    public void testClientWithJwksUri() {
-
-    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
index f57b858..fde26a1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
@@ -27,17 +27,13 @@ import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.common.Version;
-import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.common.util.Time;
 import org.keycloak.constants.AdapterConstants;
-import org.keycloak.keys.KeyProvider;
-import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.VersionRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.ComponentRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
@@ -67,7 +63,6 @@ import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.Form;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
@@ -86,7 +81,6 @@ import static org.junit.Assert.assertTrue;
 import org.keycloak.testsuite.adapter.page.CustomerPortalNoConf;
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
 import static org.keycloak.testsuite.util.WaitUtils.pause;
 import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
@@ -141,7 +135,7 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
 
     @Deployment(name = CustomerDb.DEPLOYMENT_NAME)
     protected static WebArchive customerDb() {
-        return servletDeployment(CustomerDb.DEPLOYMENT_NAME, CustomerDatabaseServlet.class);
+        return servletDeployment(CustomerDb.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class);
     }
 
     @Deployment(name = CustomerDbErrorPage.DEPLOYMENT_NAME)
@@ -218,97 +212,6 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
     }
 
     @Test
-    public void testRealmKeyRotationWithNewKeyDownload() throws Exception {
-        // Login success first
-        tokenMinTTLPage.navigateTo();
-        testRealmLoginPage.form().waitForUsernameInputPresent();
-        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
-        testRealmLoginPage.form().login("bburke@redhat.com", "password");
-        assertCurrentUrlEquals(tokenMinTTLPage);
-
-        AccessToken token = tokenMinTTLPage.getAccessToken();
-        Assert.assertEquals("bburke@redhat.com", token.getPreferredUsername());
-
-        // Logout
-        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
-                .queryParam(OAuth2Constants.REDIRECT_URI, tokenMinTTLPage.toString())
-                .build("demo").toString();
-        driver.navigate().to(logoutUri);
-
-        // Generate new realm key
-        String realmId = adminClient.realm(DEMO).toRepresentation().getId();
-        ComponentRepresentation keys = new ComponentRepresentation();
-        keys.setName("generated");
-        keys.setProviderType(KeyProvider.class.getName());
-        keys.setProviderId("rsa-generated");
-        keys.setParentId(realmId);
-        keys.setConfig(new MultivaluedHashMap<>());
-        keys.getConfig().putSingle("priority", "100");
-        Response response = adminClient.realm(DEMO).components().add(keys);
-        assertEquals(201, response.getStatus());
-        response.close();
-
-        String adapterActionsUrl = tokenMinTTLPage.toString() + "/unsecured/foo";
-        setAdapterAndServerTimeOffset(300, adapterActionsUrl);
-
-        // Try to login. Should work now due to realm key change
-        tokenMinTTLPage.navigateTo();
-        testRealmLoginPage.form().waitForUsernameInputPresent();
-        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
-        testRealmLoginPage.form().login("bburke@redhat.com", "password");
-        assertCurrentUrlEquals(tokenMinTTLPage);
-        token = tokenMinTTLPage.getAccessToken();
-        Assert.assertEquals("bburke@redhat.com", token.getPreferredUsername());
-        driver.navigate().to(logoutUri);
-
-        // Revert public keys change
-        String timeOffsetUri = UriBuilder.fromUri(adapterActionsUrl)
-                .queryParam(AdapterActionsFilter.RESET_PUBLIC_KEY_PARAM, "true")
-                .build().toString();
-        driver.navigate().to(timeOffsetUri);
-        waitUntilElement(By.tagName("body")).is().visible();
-
-        setAdapterAndServerTimeOffset(0, adapterActionsUrl);
-    }
-
-    @Test
-    public void testClientWithJwksUri() throws Exception {
-        // Set client to bad JWKS URI
-        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "secure-portal");
-        ClientRepresentation client = clientResource.toRepresentation();
-        OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
-        wrapper.setUseJwksUrl(true);
-        wrapper.setJwksUrl(securePortal + "/bad-jwks-url");
-        clientResource.update(client);
-
-        // Login should fail at the code-to-token
-        securePortal.navigateTo();
-        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
-        testRealmLoginPage.form().login("bburke@redhat.com", "password");
-        String pageSource = driver.getPageSource();
-        assertCurrentUrlStartsWith(securePortal);
-        assertFalse(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
-        // Set client to correct JWKS URI
-        client = clientResource.toRepresentation();
-        wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
-        wrapper.setUseJwksUrl(true);
-        wrapper.setJwksUrl(securePortal + "/" + AdapterConstants.K_JWKS);
-        clientResource.update(client);
-
-        // Login to secure-portal should be fine now. Client keys downloaded from JWKS URI
-        securePortal.navigateTo();
-        assertCurrentUrlEquals(securePortal);
-        pageSource = driver.getPageSource();
-        assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
-        // Logout
-        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
-                .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
-        driver.navigate().to(logoutUri);
-    }
-
-    @Test
     public void testLoginSSOAndLogout() {
         // test login to customer-portal which does a bearer request to customer-db
         customerPortal.navigateTo();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..d9116ce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.adapter.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.StreamUtil;
+import org.keycloak.common.util.Time;
+import org.keycloak.constants.AdapterConstants;
+import org.keycloak.keys.KeyProvider;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.action.GlobalRequestResult;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
+import org.keycloak.testsuite.adapter.page.CustomerDb;
+import org.keycloak.testsuite.adapter.page.SecurePortal;
+import org.keycloak.testsuite.adapter.page.TokenMinTTLPage;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.util.URLAssert;
+import org.openqa.selenium.By;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * Tests related to public key rotation for OIDC adapter
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTest {
+
+    @Page
+    private SecurePortal securePortal;
+
+    @Page
+    private TokenMinTTLPage tokenMinTTLPage;
+
+    @Page
+    private CustomerDb customerDb;
+
+    @Deployment(name = SecurePortal.DEPLOYMENT_NAME)
+    protected static WebArchive securePortal() {
+        return servletDeployment(SecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
+    }
+
+    @Deployment(name = TokenMinTTLPage.DEPLOYMENT_NAME)
+    protected static WebArchive tokenMinTTLPage() {
+        return servletDeployment(TokenMinTTLPage.DEPLOYMENT_NAME, AdapterActionsFilter.class, AbstractShowTokensServlet.class, TokenMinTTLServlet.class, ErrorServlet.class);
+    }
+
+    @Deployment(name = CustomerDb.DEPLOYMENT_NAME)
+    protected static WebArchive customerDb() {
+        return servletDeployment(CustomerDb.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class);
+    }
+
+
+
+
+    @Before
+    public void beforeRotationAdapterTest() {
+        // Delete all cookies from token-min-ttl page to be sure we are logged out
+        tokenMinTTLPage.navigateTo();
+        driver.manage().deleteAllCookies();
+    }
+
+
+    @Test
+    public void testRealmKeyRotationWithNewKeyDownload() throws Exception {
+        // Login success first
+        loginToTokenMinTtlApp();
+
+        // Logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, tokenMinTTLPage.toString())
+                .build("demo").toString();
+        driver.navigate().to(logoutUri);
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+
+        // Generate new realm key
+        generateNewRealmKey();
+
+        // Try to login again. It should fail now because not yet allowed to download new keys
+        tokenMinTTLPage.navigateTo();
+        testRealmLoginPage.form().waitForUsernameInputPresent();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        URLAssert.assertCurrentUrlStartsWith(driver, tokenMinTTLPage.getInjectedUrl().toString());
+        Assert.assertNull(tokenMinTTLPage.getAccessToken());
+
+        driver.navigate().to(logoutUri);
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+
+        setAdapterAndServerTimeOffset(300, tokenMinTTLPage.toString() + "/unsecured/foo");
+
+        // Try to login. Should work now due to realm key change
+        loginToTokenMinTtlApp();
+        driver.navigate().to(logoutUri);
+
+        // Revert public keys change
+        resetKeycloakDeploymentForAdapter(tokenMinTTLPage.toString() + "/unsecured/foo");
+    }
+
+
+    @Test
+    public void testClientWithJwksUri() throws Exception {
+        // Set client to bad JWKS URI
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "secure-portal");
+        ClientRepresentation client = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
+        wrapper.setUseJwksUrl(true);
+        wrapper.setJwksUrl(securePortal + "/bad-jwks-url");
+        clientResource.update(client);
+
+        // Login should fail at the code-to-token
+        securePortal.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        String pageSource = driver.getPageSource();
+        assertCurrentUrlStartsWith(securePortal);
+        assertFalse(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+        // Set client to correct JWKS URI
+        client = clientResource.toRepresentation();
+        wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
+        wrapper.setUseJwksUrl(true);
+        wrapper.setJwksUrl(securePortal + "/" + AdapterConstants.K_JWKS);
+        clientResource.update(client);
+
+        // Login to secure-portal should be fine now. Client keys downloaded from JWKS URI
+        securePortal.navigateTo();
+        assertCurrentUrlEquals(securePortal);
+        pageSource = driver.getPageSource();
+        assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+        // Logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
+        driver.navigate().to(logoutUri);
+    }
+
+
+    // KEYCLOAK-3824: Test for public-key-cache-ttl
+    @Test
+    public void testPublicKeyCacheTtl() {
+        driver.manage().timeouts().pageLoadTimeout(1000, TimeUnit.SECONDS);
+
+        // increase accessTokenLifespan to 1200
+        RealmRepresentation demoRealm = adminClient.realm(DEMO).toRepresentation();
+        demoRealm.setAccessTokenLifespan(1200);
+        adminClient.realm(DEMO).update(demoRealm);
+
+        // authenticate in tokenMinTTL app
+        loginToTokenMinTtlApp();
+        String accessTokenString = tokenMinTTLPage.getAccessTokenString();
+
+        // Send REST request to customer-db app. I should be successfully authenticated
+        int status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(200, status);
+
+        // Invalidate realm public key
+        generateNewRealmKey();
+
+        // Send REST request to the customer-db app. Should be still succcessfully authenticated as the JWKPublicKeyLocator cache is still valid
+        status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(200, status);
+
+        // TimeOffset to 900 on the REST app side. Token is still valid (1200) but JWKPublicKeyLocator should try to download new key (public-key-cache-ttl=600)
+        setAdapterAndServerTimeOffset(900, customerDb.toString() + "/unsecured/foo");
+
+        // Send REST request. New request to the publicKey cache should be sent, and key is no longer returned as token contains the old kid
+        status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(401, status);
+
+        // Revert public keys change and time offset
+        resetKeycloakDeploymentForAdapter(customerDb.toString() + "/unsecured/foo");
+        resetKeycloakDeploymentForAdapter(tokenMinTTLPage.toString() + "/unsecured/foo");
+    }
+
+
+    // KEYCLOAK-3823: Test that sending notBefore policy invalidates JWKPublicKeyLocator cache
+    @Test
+    public void testPublicKeyCacheInvalidatedWhenPushedNotBefore() {
+        // increase accessTokenLifespan to 1200
+        RealmRepresentation demoRealm = adminClient.realm(DEMO).toRepresentation();
+        demoRealm.setAccessTokenLifespan(1200);
+        adminClient.realm(DEMO).update(demoRealm);
+
+        // authenticate in tokenMinTTL app
+        loginToTokenMinTtlApp();
+        String accessTokenString = tokenMinTTLPage.getAccessTokenString();
+
+        // Send REST request to customer-db app. I should be successfully authenticated
+        int status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(200, status);
+
+        // Invalidate realm public key
+        generateNewRealmKey();
+
+        // Set some offset to ensure pushing notBefore will pass
+        setAdapterAndServerTimeOffset(130, customerDb.toString() + "/unsecured/foo", tokenMinTTLPage.toString() + "/unsecured/foo");
+
+        // Send REST request to the REST app. Should be still succcessfully authenticated as the JWKPublicKeyLocator cache is still valid
+        status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(200, status);
+
+        // Send notBefore policy from the realm
+        demoRealm.setNotBefore(Time.currentTime() - 1);
+        adminClient.realm(DEMO).update(demoRealm);
+        GlobalRequestResult result = adminClient.realm(DEMO).pushRevocation();
+        Assert.assertTrue(result.getSuccessRequests().contains(customerDb.toString()));
+
+        // Send REST request. New request to the publicKey cache should be sent, and key is no longer returned as token contains the old kid
+        status = invokeRESTEndpoint(accessTokenString);
+        Assert.assertEquals(401, status);
+
+        // Revert public keys change and time offset
+        resetKeycloakDeploymentForAdapter(customerDb.toString() + "/unsecured/foo");
+        resetKeycloakDeploymentForAdapter(tokenMinTTLPage.toString() + "/unsecured/foo");
+    }
+
+
+    // HELPER METHODS
+
+    private void loginToTokenMinTtlApp() {
+        tokenMinTTLPage.navigateTo();
+        testRealmLoginPage.form().waitForUsernameInputPresent();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        assertCurrentUrlEquals(tokenMinTTLPage);
+
+        AccessToken token = tokenMinTTLPage.getAccessToken();
+        Assert.assertEquals("bburke@redhat.com", token.getPreferredUsername());
+    }
+
+
+    private void generateNewRealmKey() {
+        String realmId = adminClient.realm(DEMO).toRepresentation().getId();
+
+        String oldKeyId = adminClient.realm(DEMO).components().query(realmId, KeyProvider.class.getName())
+                .get(0).getId();
+
+        ComponentRepresentation keys = new ComponentRepresentation();
+        keys.setName("generated");
+        keys.setProviderType(KeyProvider.class.getName());
+        keys.setProviderId("rsa-generated");
+        keys.setParentId(realmId);
+        keys.setConfig(new MultivaluedHashMap<>());
+        keys.getConfig().putSingle("priority", "150");
+        Response response = adminClient.realm(DEMO).components().add(keys);
+        assertEquals(201, response.getStatus());
+        response.close();
+
+        // Remove original key
+        adminClient.realm(DEMO).components().component(oldKeyId).remove();
+    }
+
+
+    private int invokeRESTEndpoint(String accessTokenString) {
+
+        HttpClient client = new DefaultHttpClient();
+        try {
+            String restUrl = customerDb.toString();
+            HttpGet get = new HttpGet(restUrl);
+            get.addHeader("Authorization", "Bearer " + accessTokenString);
+            try {
+                HttpResponse response = client.execute(get);
+                int status = response.getStatusLine().getStatusCode();
+                if (status != 200) {
+                    return status;
+                }
+
+                HttpEntity entity = response.getEntity();
+                InputStream is = entity.getContent();
+                try {
+                    String body = StreamUtil.readString(is);
+                    Assert.assertTrue(body.contains("Stian Thorgersen") && body.contains("Bill Burke"));
+                    return status;
+                } finally {
+                    is.close();
+                }
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+
+    private void resetKeycloakDeploymentForAdapter(String adapterActionsUrl) {
+        String timeOffsetUri = UriBuilder.fromUri(adapterActionsUrl)
+                .queryParam(AdapterActionsFilter.RESET_DEPLOYMENT_PARAM, "true")
+                .build().toString();
+        driver.navigate().to(timeOffsetUri);
+        waitUntilElement(By.tagName("body")).is().visible();
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowOIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..0dfd99c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowOIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter.undertow.servlet;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("auth-server-undertow")
+public class UndertowOIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/keycloak.json
index 3620170..400fac3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/keycloak.json
@@ -1,10 +1,10 @@
 {
   "realm" : "demo",
   "resource" : "customer-db",
-  "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
   "auth-server-url": "http://localhost:8180/auth",
   "ssl-required" : "external",
   "bearer-only" : true,
-  "enable-cors" : true
+  "enable-cors" : true,
+  "public-key-cache-ttl": 600
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/web.xml
index cebfe6f..56ed0e7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/web.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-db/WEB-INF/web.xml
@@ -23,11 +23,22 @@
 
     <module-name>customer-db</module-name>
 
+
+    <filter>
+        <filter-name>AdapterActionsFilter</filter-name>
+        <filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
+    </filter>
+
     <servlet>
         <servlet-name>Servlet</servlet-name>
         <servlet-class>org.keycloak.testsuite.adapter.servlet.CustomerDatabaseServlet</servlet-class>
     </servlet>
 
+    <filter-mapping>
+        <filter-name>AdapterActionsFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
     <servlet-mapping>
         <servlet-name>Servlet</servlet-name>
         <url-pattern>/*</url-pattern>
@@ -42,6 +53,12 @@
             <role-name>user</role-name>
         </auth-constraint>
     </security-constraint>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Unsecured</web-resource-name>
+            <url-pattern>/unsecured/*</url-pattern>
+        </web-resource-collection>
+    </security-constraint>
 
     <login-config>
         <auth-method>KEYCLOAK</auth-method>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
index d65fa94..b1f70e2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
@@ -118,6 +118,13 @@
     ],
     "clients": [
         {
+            "clientId": "customer-db",
+            "enabled": true,
+            "adminUrl": "/customer-db",
+            "baseUrl": "/customer-db",
+            "bearerOnly": true
+        },
+        {
             "clientId": "customer-portal",
             "enabled": true,
             "adminUrl": "/customer-portal",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
index de290de..f3fa1a5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
@@ -1,6 +1,5 @@
 {
   "realm": "demo",
-  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
   "auth-server-url": "http://localhost:8180/auth",
   "ssl-required": "external",
   "resource": "secure-portal",
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..50cc0e9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-as7")
+public class AS7OIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..df936d8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-eap")
+public class EAPOIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..607ead8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-eap6")
+public class EAP6OIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..d54889b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-wildfly")
+public class WildflyOIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..0cad5fc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-wildfly8")
+public class Wildfly8OIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OIDCPublicKeyRotationAdapterTest.java
new file mode 100644
index 0000000..a1b8118
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OIDCPublicKeyRotationAdapterTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOIDCPublicKeyRotationAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@AppServerContainer("app-server-wildfly9")
+public class Wildfly9OIDCPublicKeyRotationAdapterTest extends AbstractOIDCPublicKeyRotationAdapterTest {
+}