keycloak-developers

Changes

examples/demo-template/angular2-product-app/pom.xml 101(+0 -101)

examples/demo-template/angular2-product-app/src/main/frontend/.angular-cli.json 57(+0 -57)

examples/demo-template/angular2-product-app/src/main/frontend/.editorconfig 13(+0 -13)

examples/demo-template/angular2-product-app/src/main/frontend/.gitignore 41(+0 -41)

examples/demo-template/angular2-product-app/src/main/frontend/e2e/app.e2e-spec.ts 20(+0 -20)

examples/demo-template/angular2-product-app/src/main/frontend/e2e/app.po.ts 28(+0 -28)

examples/demo-template/angular2-product-app/src/main/frontend/e2e/tsconfig.json 19(+0 -19)

examples/demo-template/angular2-product-app/src/main/frontend/karma.conf.js 45(+0 -45)

examples/demo-template/angular2-product-app/src/main/frontend/package.json 46(+0 -46)

examples/demo-template/angular2-product-app/src/main/frontend/protractor.conf.js 31(+0 -31)

examples/demo-template/angular2-product-app/src/main/frontend/README.md 36(+0 -36)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/app.component.css 0(+0 -0)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/app.component.html 20(+0 -20)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/app.component.spec.ts 66(+0 -66)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/app.component.ts 32(+0 -32)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/app.module.ts 24(+0 -24)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/keycloak/keycloak.http.spec.ts 47(+0 -47)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/keycloak/keycloak.http.ts 42(+0 -42)

examples/demo-template/angular2-product-app/src/main/frontend/src/app/keycloak/keycloak.service.ts 60(+0 -60)

examples/demo-template/angular2-product-app/src/main/frontend/src/assets/.gitkeep 0(+0 -0)

examples/demo-template/angular2-product-app/src/main/frontend/src/environments/environment.prod.ts 5(+0 -5)

examples/demo-template/angular2-product-app/src/main/frontend/src/environments/environment.ts 10(+0 -10)

examples/demo-template/angular2-product-app/src/main/frontend/src/environments/environment.war.ts 5(+0 -5)

examples/demo-template/angular2-product-app/src/main/frontend/src/favicon.ico 0(+0 -0)

examples/demo-template/angular2-product-app/src/main/frontend/src/index.html 15(+0 -15)

examples/demo-template/angular2-product-app/src/main/frontend/src/main.ts 14(+0 -14)

examples/demo-template/angular2-product-app/src/main/frontend/src/polyfills.ts 68(+0 -68)

examples/demo-template/angular2-product-app/src/main/frontend/src/styles.css 1(+0 -1)

examples/demo-template/angular2-product-app/src/main/frontend/src/test.ts 32(+0 -32)

examples/demo-template/angular2-product-app/src/main/frontend/src/tsconfig.json 21(+0 -21)

examples/demo-template/angular2-product-app/src/main/frontend/tslint.json 116(+0 -116)

model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java 86(+0 -86)

model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java 96(+0 -96)

model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientInitialAccessPredicate.java 76(+0 -76)

Details

diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
index fabf30e..7e235ae 100644
--- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
@@ -61,15 +61,19 @@ import org.springframework.util.Assert;
  * @version $Revision: 1 $
  */
 public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
-    
+
+    public static final String DEFAULT_LOGIN_URL = "/sso/login";
     public static final String AUTHORIZATION_HEADER = "Authorization";
     public static final String SCHEME_BEARER = "bearer ";
     public static final String SCHEME_BASIC = "basic ";
 
+
     /**
-     * Request matcher that matches all requests.
+     * Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
+     * and any request with a <code>Authorization</code> header.
      */
-    private static RequestMatcher DEFAULT_REQUEST_MATCHER = new AntPathRequestMatcher("/**");
+    public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
+            new OrRequestMatcher(new AntPathRequestMatcher(DEFAULT_LOGIN_URL), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER));
 
     private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
 
@@ -107,7 +111,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
      *
      */
     public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
-                requiresAuthenticationRequestMatcher) {
+            requiresAuthenticationRequestMatcher) {
         super(requiresAuthenticationRequestMatcher);
         Assert.notNull(authenticationManager, "authenticationManager cannot be null");
         this.authenticationManager = authenticationManager;
@@ -138,20 +142,27 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
         log.debug("Auth outcome: {}", result);
 
         if (AuthOutcome.FAILED.equals(result)) {
-        	AuthChallenge challenge = authenticator.getChallenge();
+            AuthChallenge challenge = authenticator.getChallenge();
             if (challenge != null) {
                 challenge.challenge(facade);
             }
             throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details");
         }
+
         if (AuthOutcome.NOT_ATTEMPTED.equals(result)) {
-        	AuthChallenge challenge = authenticator.getChallenge();
+            AuthChallenge challenge = authenticator.getChallenge();
             if (challenge != null) {
                 challenge.challenge(facade);
             }
-            throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
+            if (deployment.isBearerOnly()) {
+                // no redirection in this mode, throwing exception for the spring handler
+                throw new KeycloakAuthenticationException("Authorization header not found,  see WWW-Authenticate header");
+            } else {
+                // let continue if challenged, it may redirect
+                return null;
+            }
         }
-       
+
         else if (AuthOutcome.AUTHENTICATED.equals(result)) {
             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
             Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
@@ -193,7 +204,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
 
     @Override
     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
-            Authentication authResult) throws IOException, ServletException {
+                                            Authentication authResult) throws IOException, ServletException {
 
         if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
             super.successfulAuthentication(request, response, chain, authResult);
@@ -220,10 +231,10 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
     }
 
     @Override
-	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
-			AuthenticationException failed) throws IOException, ServletException {
-		super.unsuccessfulAuthentication(request, response, failed);
-	}
+    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+                                              AuthenticationException failed) throws IOException, ServletException {
+        super.unsuccessfulAuthentication(request, response, failed);
+    }
 
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
@@ -259,4 +270,4 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
     public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
         throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
     }
-}
+}
\ No newline at end of file
diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java
new file mode 100644
index 0000000..f434b97
--- /dev/null
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java
@@ -0,0 +1,25 @@
+package org.keycloak.adapters.springsecurity;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Add this annotation to a class that extends {@code KeycloakWebSecurityConfigurerAdapter} to provide
+ * a keycloak based Spring security configuration.
+ *
+ * @author Hendrik Ebbers
+ */
+@Retention(value = RUNTIME)
+@Target(value = { TYPE })
+@Configuration
+@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
+@EnableWebSecurity
+public @interface KeycloakConfiguration {
+}
diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
index 2414c38..a6a378a 100755
--- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
+++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
@@ -159,12 +159,10 @@ public class KeycloakAuthenticationProcessingFilterTest {
         when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
         when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
         when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
-        try {
-        	filter.attemptAuthentication(request, response);
-        } catch (KeycloakAuthenticationException e) {
-        	verify(response).setStatus(302);
-            verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
-        }
+
+        filter.attemptAuthentication(request, response);
+        verify(response).setStatus(302);
+        verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
     }
 
     @Test(expected = KeycloakAuthenticationException.class)
@@ -173,6 +171,13 @@ public class KeycloakAuthenticationProcessingFilterTest {
         filter.attemptAuthentication(request, response);
     }
 
+    @Test(expected = KeycloakAuthenticationException.class)
+    public void testAttemptAuthenticationWithInvalidTokenBearerOnly() throws Exception {
+        when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.TRUE);
+        request.addHeader("Authorization", "Bearer xxx");
+        filter.attemptAuthentication(request, response);
+    }
+
     @Test
     public void testSuccessfulAuthenticationInteractive() throws Exception {
         Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities);
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
index 256bfa2..ecde04a 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
@@ -33,6 +33,7 @@ public class ConfigXmlConstants {
     public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
     public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
     public static final String LOGOUT_PAGE_ATTR = "logoutPage";
+    public static final String AUTODETECT_BEARER_ONLY_ATTR = "autodetectBearerOnly";
 
     public static final String KEYS_ELEMENT = "Keys";
     public static final String KEY_ELEMENT = "Key";
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
index b5f8aba..92254a2 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
@@ -68,6 +68,7 @@ public class DeploymentBuilder {
         deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
         deployment.setLogoutPage(sp.getLogoutPage());
         deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
+        deployment.setAutodetectBearerOnly(sp.isAutodetectBearerOnly());
         deployment.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256);
         if (sp.getIdp().getSignatureAlgorithm() != null) {
             deployment.setSignatureAlgorithm(SignatureAlgorithm.valueOf(sp.getIdp().getSignatureAlgorithm()));
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
index be6d682..57d5f58 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
@@ -89,6 +89,7 @@ public class SPXmlParser extends AbstractParser {
         sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
         sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
         sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
+        sp.setAutodetectBearerOnly(getBooleanAttributeValue(startElement, ConfigXmlConstants.AUTODETECT_BEARER_ONLY_ATTR));
         sp.setTurnOffChangeSessionIdOnLogin(getBooleanAttributeValue(startElement, ConfigXmlConstants.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN_ATTR));
         while (xmlEventReader.hasNext()) {
             XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
index 475a0f4..071b170 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
@@ -58,6 +58,7 @@ public class SP implements Serializable {
     private PrincipalNameMapping principalNameMapping;
     private Set<String> roleAttributes;
     private IDP idp;
+    private boolean autodetectBearerOnly;
 
     public String getEntityID() {
         return entityID;
@@ -147,4 +148,11 @@ public class SP implements Serializable {
         this.logoutPage = logoutPage;
     }
 
+    public boolean isAutodetectBearerOnly() {
+        return autodetectBearerOnly;
+    }
+
+    public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
+        this.autodetectBearerOnly = autodetectBearerOnly;
+    }
 }
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
index d92884f..9a6e288 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
@@ -294,6 +294,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
     private String logoutPage;
     private SignatureAlgorithm signatureAlgorithm;
     private String signatureCanonicalizationMethod;
+    private boolean autodetectBearerOnly;
 
     @Override
     public boolean turnOffChangeSessionIdOnLogin() {
@@ -439,4 +440,13 @@ public class DefaultSamlDeployment implements SamlDeployment {
     public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
         this.signatureAlgorithm = signatureAlgorithm;
     }
+
+    @Override
+    public boolean isAutodetectBearerOnly() {
+        return autodetectBearerOnly;
+    }
+
+    public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
+        this.autodetectBearerOnly = autodetectBearerOnly;
+    }
 }
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index 5721b03..08ce4a9 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -364,26 +364,26 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
 
         if (deployment.getIDP().getSingleSignOnService().validateAssertionSignature()) {
             try {
-                validateSamlSignature(new SAMLDocumentHolder(buildAssertionDocument(responseHolder, assertion)), postBinding, GeneralConstants.SAML_RESPONSE_KEY);
-            } catch (VerificationException e) {
-                log.error("Failed to verify saml assertion signature", e);
+                if (!AssertionUtil.isSignatureValid(getAssertionFromResponse(responseHolder), deployment.getIDP().getSignatureValidationKeyLocator())) {
+                    log.error("Failed to verify saml assertion signature");
 
-                challenge = new AuthChallenge() {
+                    challenge = new AuthChallenge() {
 
-                    @Override
-                    public boolean challenge(HttpFacade exchange) {
-                        SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE, responseType);
-                        exchange.getRequest().setError(error);
-                        exchange.getResponse().sendError(403);
-                        return true;
-                    }
+                        @Override
+                        public boolean challenge(HttpFacade exchange) {
+                            SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE, responseType);
+                            exchange.getRequest().setError(error);
+                            exchange.getResponse().sendError(403);
+                            return true;
+                        }
 
-                    @Override
-                    public int getResponseCode() {
-                        return 403;
-                    }
-                };
-                return AuthOutcome.FAILED;
+                        @Override
+                        public int getResponseCode() {
+                            return 403;
+                        }
+                    };
+                    return AuthOutcome.FAILED;
+                }
             } catch (Exception e) {
                 log.error("Error processing validation of SAML assertion: " + e.getMessage());
                 challenge = new AuthChallenge() {
@@ -504,19 +504,16 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
           && Objects.equals(responseType.getStatus().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
     }
 
-    private Document buildAssertionDocument(final SAMLDocumentHolder responseHolder, AssertionType assertion) throws ConfigurationException, ProcessingException {
-        Element encryptedAssertion = org.keycloak.saml.common.util.DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
+    private Element getAssertionFromResponse(final SAMLDocumentHolder responseHolder) throws ConfigurationException, ProcessingException {
+        Element encryptedAssertion = DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
         if (encryptedAssertion != null) {
             // encrypted assertion.
             // We'll need to decrypt it first.
             Document encryptedAssertionDocument = DocumentUtil.createDocument();
             encryptedAssertionDocument.appendChild(encryptedAssertionDocument.importNode(encryptedAssertion, true));
-            Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(encryptedAssertionDocument, deployment.getDecryptionKey());
-            Document assertionDocument = DocumentUtil.createDocument();
-            assertionDocument.appendChild(assertionDocument.importNode(assertionElement, true));
-            return assertionDocument;
+            return XMLEncryptionUtil.decryptElementInDocument(encryptedAssertionDocument, deployment.getDecryptionKey());
         }
-        return AssertionUtil.asDocument(assertion);
+        return DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get()));
     }
 
     private String getAttributeValue(Object attrValue) {
@@ -568,9 +565,15 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
         return new AbstractInitiateLogin(deployment, sessionStore) {
             @Override
             protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException {
-                Document document = authnRequestBuilder.toDocument();
-                SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
-                SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
+                if (isAutodetectedBearerOnly(httpFacade.getRequest())) {
+                    httpFacade.getResponse().setStatus(401);
+                    httpFacade.getResponse().end();
+                }
+                else {
+                    Document document = authnRequestBuilder.toDocument();
+                    SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
+                    SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
+                }
             }
         };
     }
@@ -693,4 +696,34 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
 
         return signature.verify(decodedSignature);
     }
+
+    protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
+        if (!deployment.isAutodetectBearerOnly()) return false;
+
+        String headerValue = facade.getRequest().getHeader(GeneralConstants.HTTP_HEADER_X_REQUESTED_WITH);
+        if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
+            return true;
+        }
+
+        headerValue = facade.getRequest().getHeader("Faces-Request");
+        if (headerValue != null && headerValue.startsWith("partial/")) {
+            return true;
+        }
+
+        headerValue = facade.getRequest().getHeader("SOAPAction");
+        if (headerValue != null) {
+            return true;
+        }
+
+        List<String> accepts = facade.getRequest().getHeaders("Accept");
+        if (accepts == null) accepts = Collections.emptyList();
+
+        for (String accept : accepts) {
+            if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
index a9df7e9..0b7ac40 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
@@ -168,6 +168,6 @@ public interface SamlDeployment {
     }
     PrincipalNamePolicy getPrincipalNamePolicy();
     String getPrincipalAttributeName();
-
+    boolean isAutodetectBearerOnly();
 
 }
diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd
new file mode 100644
index 0000000..cf9c05c
--- /dev/null
+++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_9.xsd
@@ -0,0 +1,461 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  ~ 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.
+  -->
+
+<xs:schema version="1.0"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           xmlns="urn:keycloak:saml:adapter"
+           targetNamespace="urn:keycloak:saml:adapter"
+           elementFormDefault="qualified"
+           attributeFormDefault="unqualified">
+
+    <xs:element name="keycloak-saml-adapter" type="adapter-type"/>
+    <xs:complexType name="adapter-type">
+        <xs:annotation>
+            <xs:documentation>Keycloak SAML Adapter configuration file.</xs:documentation>
+        </xs:annotation>
+        <xs:all>
+            <xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type">
+                <xs:annotation>
+                    <xs:documentation>Describes SAML service provider configuration.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:all>
+    </xs:complexType>
+
+    <xs:complexType name="sp-type">
+        <xs:all>
+            <xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        List of service provider encryption and validation keys.
+
+                        If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user.
+                    By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Describes configuration of SAML identity provider for this service provider.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:all>
+        <xs:attribute name="entityID" type="xs:string" use="required">
+                <xs:annotation>
+                    <xs:documentation>This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="sslPolicy" type="ssl-policy-type" use="optional">
+                <xs:annotation>
+                    <xs:documentation>SSL policy the adapter will enforce.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional">
+                <xs:annotation>
+                    <xs:documentation>SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="logoutPage" type="xs:string" use="optional">
+                <xs:annotation>
+                    <xs:documentation>URL of the logout page.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
+                <xs:annotation>
+                    <xs:documentation>SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="isPassive" type="xs:boolean" use="optional">
+                <xs:annotation>
+                    <xs:documentation>SAML clients can request that a user is never asked to authenticate even if they are not logged in at the IDP. Set this to true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
+                <xs:annotation>
+                    <xs:documentation>The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="autodetectBearerOnly" type="xs:boolean" use="optional" default="false">
+                <xs:annotation>
+                  <xs:documentation>This should be set to true if your application serves both a web application and web services (e.g. SOAP or REST). It allows you to redirect unauthenticated users of the web application to the Keycloak login page, but send an HTTP 401 status code to unauthenticated SOAP or REST clients instead as they would not understand a redirect to the login page. Keycloak auto-detects SOAP or REST clients based on typical headers like X-Requested-With, SOAPAction or Accept. The default value is false.</xs:documentation>
+                </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="keys-type">
+        <xs:sequence>
+            <xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded">
+                <xs:annotation>
+                    <xs:documentation>Describes a single key used for signing or encryption.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="key-type">
+        <xs:all>
+            <xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type">
+                <xs:annotation>
+                    <xs:documentation>Java keystore to load keys and certificates from.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Private key (PEM format)</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Public key (PEM format)</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Certificate key (PEM format)</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:all>
+        <xs:attribute name="signing" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Flag defining whether the key should be used for signing.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="encryption" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Flag defining whether the key should be used for encryption</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="key-store-type">
+        <xs:all>
+            <xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type">
+                <xs:annotation>
+                    <xs:documentation>Private key declaration</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Certificate declaration</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:all>
+        <xs:attribute name="file" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>File path to the key store.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="resource" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream().</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="password" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>The password of the key store.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="private-key-type">
+        <xs:attribute name="alias" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="password" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="certificate-type">
+        <xs:attribute name="alias" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="principal-name-mapping-type">
+        <xs:attribute name="policy" type="principal-name-mapping-policy-type" use="required">
+            <xs:annotation>
+                <xs:documentation>Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal().</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="attribute" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>Name of the SAML assertion attribute to use within.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:simpleType name="principal-name-mapping-policy-type">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="FROM_NAME_ID">
+                <xs:annotation>
+                    <xs:documentation>This policy just uses whatever the SAML subject value is. This is the default setting</xs:documentation>
+                </xs:annotation>
+            </xs:enumeration>
+            <xs:enumeration value="FROM_ATTRIBUTE">
+                <xs:annotation>
+                    <xs:documentation>This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute.</xs:documentation>
+                </xs:annotation>
+            </xs:enumeration>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:simpleType name="ssl-policy-type">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="ALL">
+                <xs:annotation>
+                    <xs:documentation>All requests must come in via HTTPS.</xs:documentation>
+                </xs:annotation>
+            </xs:enumeration>
+            <xs:enumeration value="EXTERNAL">
+                <xs:annotation>
+                    <xs:documentation>Only non-private IP addresses must come over the wire via HTTPS.</xs:documentation>
+                </xs:annotation>
+            </xs:enumeration>
+            <xs:enumeration value="NONE">
+                <xs:annotation>
+                    <xs:documentation>no requests are required to come over via HTTPS.</xs:documentation>
+                </xs:annotation>
+            </xs:enumeration>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:simpleType name="signature-algorithm-type">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="RSA_SHA1"/>
+            <xs:enumeration value="RSA_SHA256"/>
+            <xs:enumeration value="RSA_SHA512"/>
+            <xs:enumeration value="DSA_SHA1"/>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:simpleType name="binding-type">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="POST"/>
+            <xs:enumeration value="REDIRECT"/>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:complexType name="role-identifiers-type">
+        <xs:choice minOccurs="0" maxOccurs="unbounded">
+            <xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type">
+                <xs:annotation>
+                    <xs:documentation>Specifies SAML attribute to be converted into roles.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:choice>
+    </xs:complexType>
+    <xs:complexType name="attribute-type">
+        <xs:attribute name="name" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>Specifies name of the SAML attribute to be converted into roles.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="idp-type">
+        <xs:sequence minOccurs="0" maxOccurs="unbounded">
+            <xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type">
+                <xs:annotation>
+                    <xs:documentation>Configuration of the login SAML endpoint of the IDP.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Configuration of the logout SAML endpoint of the IDP</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+            <xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP.</xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="entityID" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>issuer ID of the IDP.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="signatureAlgorithm" type="signature-algorithm-type" use="optional">
+            <xs:annotation>
+                <xs:documentation>Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the signature canonicalization method that the IDP expects signed documents to use. The default value is https://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="encryption" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation></xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+    <xs:complexType name="sign-on-type">
+        <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="requestBinding" type="binding-type" use="optional">
+            <xs:annotation>
+                <xs:documentation>SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="responseBinding" type="binding-type" use="optional">
+            <xs:annotation>
+                <xs:documentation>SAML allows the client to request what binding type it wants authn responses to use. This value maps to ProtocolBinding attribute in SAML AuthnRequest. The default is that the client will not request a specific binding type for responses.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="bindingUrl" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>This is the URL for the IDP login service that the client will send requests to.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>URL of the assertion consumer service (ACS) where the IDP login service should send responses to. By default it is unset, relying on the IdP settings. When set, it must end in "/saml". This property is typically accompanied by the responseBinding attribute.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="logout-type">
+        <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="signResponse" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+            <xs:annotation>
+                <xs:documentation>Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="requestBinding" type="binding-type" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="responseBinding" type="binding-type" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="postBindingUrl" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="http-client-type">
+        <xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional" default="false">
+            <xs:annotation>
+                <xs:documentation>If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate
+                    is validated via the truststore, but host name validation is not done. This setting should only be used during
+                    development and never in production as it will partly disable verification of SSL certificates.
+                    This seting may be useful in test environments. The default value is false.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="clientKeystore" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>This is the file path to a keystore file. This keystore contains client certificate 
+                    for two-way SSL when the adapter makes HTTPS requests to the IDP server.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="clientKeystorePassword" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>Password for the client keystore and for the client's key.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="connectionPoolSize" type="xs:int" use="optional" default="10">
+            <xs:annotation>
+                <xs:documentation>Defines number of pooled connections.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="disableTrustManager" type="xs:boolean" use="optional" default="false">
+            <xs:annotation>
+                <xs:documentation>If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore.
+                    This setting should only be used during development and never in production as it will disable verification of SSL certificates.
+                    The default value is false.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="proxyUrl" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>URL to HTTP proxy to use for HTTP connections.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="truststore" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>The value is the file path to a keystore file. If you prefix the path with classpath:,
+                    then the truststore will be obtained from the deployment's classpath instead. Used for outgoing 
+                    HTTPS communications to the IDP server. Client making HTTPS requests need
+                    a way to verify the host of the server they are talking to. This is what the trustore does.
+                    The keystore contains one or more trusted host certificates or certificate authorities.
+                    You can create this truststore by extracting the public certificate of the IDP's SSL keystore.
+                </xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+        <xs:attribute name="truststorePassword" type="xs:string" use="optional">
+            <xs:annotation>
+                <xs:documentation>Password for the truststore keystore.</xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+</xs:schema>
diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java
index b64f181..88a96cb 100755
--- a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java
+++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java
@@ -37,7 +37,7 @@ import org.keycloak.saml.common.exceptions.ParsingException;
  */
 public class KeycloakSamlAdapterXMLParserTest {
 
-    private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
+    private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_9.xsd";
 
     @Rule
     public ExpectedException expectedException = ExpectedException.none();
@@ -91,6 +91,7 @@ public class KeycloakSamlAdapterXMLParserTest {
         assertEquals("format", sp.getNameIDPolicyFormat());
         assertTrue(sp.isForceAuthentication());
         assertTrue(sp.isIsPassive());
+        assertFalse(sp.isAutodetectBearerOnly());
         assertEquals(2, sp.getKeys().size());
         Key signing = sp.getKeys().get(0);
         assertTrue(signing.isSigning());
diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml.xml
index 193525a..31f62e5 100755
--- a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml.xml
+++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml.xml
@@ -17,7 +17,7 @@
 
 <keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
                        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-                       xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_7.xsd">
+                       xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_9.xsd">
     <SP entityID="sp"
         sslPolicy="EXTERNAL"
         nameIDPolicyFormat="format"
@@ -73,4 +73,4 @@
             </Keys>
         </IDP>
     </SP>
-</keycloak-saml-adapter>
\ No newline at end of file
+</keycloak-saml-adapter>
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
new file mode 100644
index 0000000..2ae1bdc
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 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.authorization.policy.provider.group;
+
+import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
+
+import java.util.function.Function;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProvider implements PolicyProvider {
+
+    private final Function<Policy, GroupPolicyRepresentation> representationFunction;
+
+    public GroupPolicyProvider(Function<Policy, GroupPolicyRepresentation> representationFunction) {
+        this.representationFunction = representationFunction;
+    }
+
+    @Override
+    public void evaluate(Evaluation evaluation) {
+        GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy());
+        RealmModel realm = evaluation.getAuthorizationProvider().getRealm();
+        Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim());
+
+        if (groupsClaim == null || groupsClaim.isEmpty()) {
+            return;
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) {
+            GroupModel allowedGroup = realm.getGroupById(definition.getId());
+
+            for (int i = 0; i < groupsClaim.size(); i++) {
+                String group = groupsClaim.asString(i);
+
+                if (group.indexOf('/') != -1) {
+                    String allowedGroupPath = buildGroupPath(allowedGroup);
+                    if (group.equals(allowedGroupPath) || (definition.isExtendChildren() && group.startsWith(allowedGroupPath))) {
+                        evaluation.grant();
+                        return;
+                    }
+                }
+
+                // in case the group from the claim does not represent a path, we just check an exact name match
+                if (group.equals(allowedGroup.getName())) {
+                    evaluation.grant();
+                    return;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
\ No newline at end of file
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
new file mode 100644
index 0000000..f558449
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2017 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.authorization.policy.provider.group;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPolicyRepresentation> {
+
+    private GroupPolicyProvider provider = new GroupPolicyProvider(policy -> toRepresentation(policy, new GroupPolicyRepresentation()));
+
+    @Override
+    public String getId() {
+        return "group";
+    }
+
+    @Override
+    public String getName() {
+        return "Group";
+    }
+
+    @Override
+    public String getGroup() {
+        return "Identity Based";
+    }
+
+    @Override
+    public PolicyProvider create(AuthorizationProvider authorization) {
+        return provider;
+    }
+
+    @Override
+    public PolicyProvider create(KeycloakSession session) {
+        return provider;
+    }
+
+    @Override
+    public GroupPolicyRepresentation toRepresentation(Policy policy, GroupPolicyRepresentation representation) {
+        representation.setGroupsClaim(policy.getConfig().get("groupsClaim"));
+        try {
+            representation.setGroups(getGroupsDefinition(policy.getConfig()));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+        return representation;
+    }
+
+    @Override
+    public Class<GroupPolicyRepresentation> getRepresentationType() {
+        return GroupPolicyRepresentation.class;
+    }
+
+    @Override
+    public void onCreate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onUpdate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
+        try {
+            updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig()), authorization);
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+    }
+
+    @Override
+    public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
+        Map<String, String> config = new HashMap<>();
+        GroupPolicyRepresentation groupPolicy = toRepresentation(policy, new GroupPolicyRepresentation());
+        Set<GroupPolicyRepresentation.GroupDefinition> groups = groupPolicy.getGroups();
+
+        for (GroupPolicyRepresentation.GroupDefinition definition: groups) {
+            GroupModel group = authorizationProvider.getRealm().getGroupById(definition.getId());
+            definition.setId(null);
+            definition.setPath(ModelToRepresentation.buildGroupPath(group));
+        }
+
+        try {
+            config.put("groupsClaim", groupPolicy.getGroupsClaim());
+            config.put("groups", JsonSerialization.writeValueAsString(groups));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to export group policy [" + policy.getName() + "]", cause);
+        }
+
+        representation.setConfig(config);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+        factory.register(event -> {
+        });
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    private void updatePolicy(Policy policy, String groupsClaim, Set<GroupPolicyRepresentation.GroupDefinition> groups, AuthorizationProvider authorization) {
+        if (groupsClaim == null) {
+            throw new RuntimeException("Group claims property not provided");
+        }
+
+        if (groups == null || groups.isEmpty()) {
+            throw new RuntimeException("You must provide at least one group");
+        }
+
+        Map<String, String> config = new HashMap<>(policy.getConfig());
+
+        config.put("groupsClaim", groupsClaim);
+
+        List<GroupModel> topLevelGroups = authorization.getRealm().getTopLevelGroups();
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : groups) {
+            GroupModel group = null;
+
+            if (definition.getId() != null) {
+                group = authorization.getRealm().getGroupById(definition.getId());
+            }
+
+            if (group == null) {
+                String path = definition.getPath();
+                String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
+
+                if (canonicalPath != null) {
+                    String[] parts = canonicalPath.split("/");
+                    GroupModel parent = null;
+
+                    for (String part : parts) {
+                        if (parent == null) {
+                            parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found"));
+                        } else {
+                            group = parent.getSubGroups().stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found"));
+                            parent = group;
+                        }
+                    }
+
+                    if (parts.length == 1) {
+                        group = parent;
+                    }
+                }
+            }
+
+            if (group == null) {
+                throw new RuntimeException("Group with id [" + definition.getId() + "] not found");
+            }
+
+            definition.setId(group.getId());
+            definition.setPath(null);
+        }
+
+        try {
+            config.put("groups", JsonSerialization.writeValueAsString(groups));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to serialize groups", cause);
+        }
+
+        policy.setConfig(config);
+    }
+
+    private HashSet<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config) throws IOException {
+        return new HashSet<>(Arrays.asList(JsonSerialization.readValue(config.get("groups"), GroupPolicyRepresentation.GroupDefinition[].class)));
+    }
+}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
index f875731..944ae02 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
@@ -17,43 +17,44 @@
  */
 package org.keycloak.authorization.policy.provider.js;
 
-import java.util.function.Supplier;
-
-import javax.script.ScriptEngine;
-import javax.script.ScriptException;
+import java.util.function.BiFunction;
 
+import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.policy.evaluation.Evaluation;
 import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.scripting.EvaluatableScriptAdapter;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
-public class JSPolicyProvider implements PolicyProvider {
+class JSPolicyProvider implements PolicyProvider {
 
-    private Supplier<ScriptEngine> engineProvider;
+    private final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript;
 
-    public JSPolicyProvider(Supplier<ScriptEngine> engineProvider) {
-        this.engineProvider = engineProvider;
+    JSPolicyProvider(final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript) {
+        this.evaluatableScript = evaluatableScript;
     }
 
     @Override
     public void evaluate(Evaluation evaluation) {
-        ScriptEngine engine = engineProvider.get();
-
-        engine.put("$evaluation", evaluation);
-
         Policy policy = evaluation.getPolicy();
+        AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
+        final EvaluatableScriptAdapter adapter = evaluatableScript.apply(authorization, policy);
 
         try {
-            engine.eval(policy.getConfig().get("code"));
-        } catch (ScriptException e) {
+            //how to deal with long running scripts -> timeout?
+            adapter.eval(bindings -> {
+                bindings.put("script", adapter.getScriptModel());
+                bindings.put("$evaluation", evaluation);
+            });
+        }
+        catch (Exception e) {
             throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e);
         }
     }
 
     @Override
     public void close() {
-
     }
 }
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
index 3e68d7f..e3e82ce 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
@@ -1,9 +1,5 @@
 package org.keycloak.authorization.policy.provider.js;
 
-import java.util.Map;
-
-import javax.script.ScriptEngineManager;
-
 import org.keycloak.Config;
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
@@ -11,17 +7,20 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.ScriptModel;
 import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+import org.keycloak.scripting.ScriptingProvider;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
 public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRepresentation> {
 
-    private static final String ENGINE = "nashorn";
-
-    private JSPolicyProvider provider = new JSPolicyProvider(() -> new ScriptEngineManager().getEngineByName(ENGINE));
+    private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript);
+    private ScriptCache scriptCache;
 
     @Override
     public String getName() {
@@ -69,13 +68,16 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
         updatePolicy(policy, representation.getConfig().get("code"));
     }
 
-    private void updatePolicy(Policy policy, String code) {
-        policy.putConfig("code", code);
+    @Override
+    public void onRemove(final Policy policy, final AuthorizationProvider authorization) {
+        scriptCache.remove(policy.getId());
     }
 
     @Override
     public void init(Config.Scope config) {
-
+        int maxEntries = Integer.parseInt(config.get("cache-max-entries", "100"));
+        int maxAge = Integer.parseInt(config.get("cache-entry-max-age", "-1"));
+        scriptCache = new ScriptCache(maxEntries, maxAge);
     }
 
     @Override
@@ -92,4 +94,26 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
     public String getId() {
         return "js";
     }
+
+    private EvaluatableScriptAdapter getEvaluatableScript(final AuthorizationProvider authz, final Policy policy) {
+        return scriptCache.computeIfAbsent(policy.getId(), id -> {
+            final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class);
+            ScriptModel script = getScriptModel(policy, authz.getRealm(), scripting);
+            return scripting.prepareEvaluatableScript(script);
+        });
+    }
+
+    private ScriptModel getScriptModel(final Policy policy, final RealmModel realm, final ScriptingProvider scripting) {
+        String scriptName = policy.getName();
+        String scriptCode = policy.getConfig().get("code");
+        String scriptDescription = policy.getDescription();
+
+        //TODO lookup script by scriptId instead of creating it every time
+        return scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
+    }
+
+    private void updatePolicy(Policy policy, String code) {
+        scriptCache.remove(policy.getId());
+        policy.putConfig("code", code);
+    }
 }
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
new file mode 100644
index 0000000..4180db6
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 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.authorization.policy.provider.js;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Function;
+
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScriptCache {
+
+    /**
+     * The load factor.
+     */
+    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+    private final Map<String, CacheEntry> cache;
+
+    private final AtomicBoolean writing = new AtomicBoolean(false);
+
+    private final long maxAge;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param maxEntries the maximum number of entries to keep in the cache
+     */
+    public ScriptCache(int maxEntries) {
+        this(maxEntries, -1);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param maxEntries the maximum number of entries to keep in the cache
+     * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
+     */
+    public ScriptCache(final int maxEntries, long maxAge) {
+        cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry eldest) {
+                return cache.size()  > maxEntries;
+            }
+        };
+        this.maxAge = maxAge;
+    }
+
+    public EvaluatableScriptAdapter computeIfAbsent(String id, Function<String, EvaluatableScriptAdapter> function) {
+        try {
+            if (parkForWriteAndCheckInterrupt()) {
+                return null;
+            }
+
+            CacheEntry entry = cache.computeIfAbsent(id, key -> new CacheEntry(key, function.apply(id), maxAge));
+
+            if (entry != null) {
+                return entry.value();
+            }
+
+            return null;
+        } finally {
+            writing.lazySet(false);
+        }
+    }
+
+    public EvaluatableScriptAdapter get(String uri) {
+        if (parkForReadAndCheckInterrupt()) {
+            return null;
+        }
+
+        CacheEntry cached = cache.get(uri);
+
+        if (cached != null) {
+            return removeIfExpired(cached);
+        }
+
+        return null;
+    }
+
+    public void remove(String key) {
+        try {
+            if (parkForWriteAndCheckInterrupt()) {
+                return;
+            }
+
+            cache.remove(key);
+        } finally {
+            writing.lazySet(false);
+        }
+    }
+
+    private EvaluatableScriptAdapter removeIfExpired(CacheEntry cached) {
+        if (cached == null) {
+            return null;
+        }
+
+        if (cached.isExpired()) {
+            remove(cached.key());
+            return null;
+        }
+
+        return cached.value();
+    }
+
+    private boolean parkForWriteAndCheckInterrupt() {
+        while (!writing.compareAndSet(false, true)) {
+            LockSupport.parkNanos(1L);
+            if (Thread.interrupted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean parkForReadAndCheckInterrupt() {
+        while (writing.get()) {
+            LockSupport.parkNanos(1L);
+            if (Thread.interrupted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static final class CacheEntry {
+
+        final String key;
+        final EvaluatableScriptAdapter value;
+        final long expiration;
+
+        CacheEntry(String key, EvaluatableScriptAdapter value, long maxAge) {
+            this.key = key;
+            this.value = value;
+            if(maxAge == -1) {
+                expiration = -1;
+            } else {
+                expiration = System.currentTimeMillis() + maxAge;
+            }
+        }
+
+        String key() {
+            return key;
+        }
+
+        EvaluatableScriptAdapter value() {
+            return value;
+        }
+
+        boolean isExpired() {
+            return expiration != -1 ? System.currentTimeMillis() > expiration : false;
+        }
+    }
+}
diff --git a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
index e4588f8..e6fa1cc 100644
--- a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
+++ b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -41,4 +41,5 @@ org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory
 org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
 org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
 org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
-org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
\ No newline at end of file
+org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
+org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
\ No newline at end of file
diff --git a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
index c092c6f..258f076 100755
--- a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
+++ b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
@@ -31,9 +31,9 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
 {
    public void putSingle(K key, V value)
    {
-      List<V> list = new CopyOnWriteArrayList<>();
+      List<V> list = createListInstance();
       list.add(value);
-      put(key, list);
+      put(key, list); // Just override with new List instance
    }
 
    public void addAll(K key, V... newValues)
@@ -84,8 +84,15 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
    public final List<V> getList(K key)
    {
       List<V> list = get(key);
-      if (list == null)
-         put(key, list = new CopyOnWriteArrayList<V>());
+
+      if (list == null) {
+         list = createListInstance();
+         List<V> existing = putIfAbsent(key, list);
+         if (existing != null) {
+            list = existing;
+         }
+      }
+
       return list;
    }
 
@@ -97,4 +104,8 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
       }
    }
 
+   protected List<V> createListInstance() {
+      return new CopyOnWriteArrayList<>();
+   }
+
 }
diff --git a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java
index 1fdfff8..78fc4c7 100644
--- a/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java
+++ b/common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.common.util;
 
+import java.util.Collections;
+import java.util.Map;
 import java.util.Properties;
 
 /**
@@ -24,9 +26,21 @@ import java.util.Properties;
  */
 public class SystemEnvProperties extends Properties {
 
+    private final Map<String, String> overrides;
+
+    public SystemEnvProperties(Map<String, String> overrides) {
+        this.overrides = overrides;
+    }
+
+    public SystemEnvProperties() {
+        this.overrides = Collections.EMPTY_MAP;
+    }
+
     @Override
     public String getProperty(String key) {
-        if (key.startsWith("env.")) {
+        if (overrides.containsKey(key)) {
+            return overrides.get(key);
+        } else if (key.startsWith("env.")) {
             return System.getenv().get(key.substring(4));
         } else {
             return System.getProperty(key);
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
new file mode 100644
index 0000000..c063f8f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 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.representations.idm.authorization;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyRepresentation extends AbstractPolicyRepresentation {
+
+    private String groupsClaim;
+    private Set<GroupDefinition> groups;
+
+    @Override
+    public String getType() {
+        return "group";
+    }
+
+    public String getGroupsClaim() {
+        return groupsClaim;
+    }
+
+    public void setGroupsClaim(String groupsClaim) {
+        this.groupsClaim = groupsClaim;
+    }
+
+    public Set<GroupDefinition> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(Set<GroupDefinition> groups) {
+        this.groups = groups;
+    }
+
+    public void addGroup(String... ids) {
+        for (String id : ids) {
+            addGroup(id, false);
+        }
+    }
+
+    public void addGroup(String id, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(id, extendChildren));
+    }
+
+    public void addGroupPath(String... paths) {
+        for (String path : paths) {
+            addGroupPath(path, false);
+        }
+    }
+
+    public void addGroupPath(String path, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(null, path, extendChildren));
+    }
+
+    public void removeGroup(String... ids) {
+        if (groups != null) {
+            for (final String id : ids) {
+                if (!groups.remove(id)) {
+                    for (GroupDefinition group : new HashSet<>(groups)) {
+                        if (group.getPath().startsWith(id)) {
+                            groups.remove(group);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static class GroupDefinition {
+
+        private String id;
+        private String path;
+        private boolean extendChildren;
+
+        public GroupDefinition() {
+            this(null);
+        }
+
+        public GroupDefinition(String id) {
+            this(id, false);
+        }
+
+        public GroupDefinition(String id, boolean extendChildren) {
+            this(id, null, extendChildren);
+        }
+
+        public GroupDefinition(String id, String path, boolean extendChildren) {
+            this.id = id;
+            this.path = path;
+            this.extendChildren = extendChildren;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getPath() {
+            return path;
+        }
+
+        public void setPath(String path) {
+            this.path = path;
+        }
+
+        public boolean isExtendChildren() {
+            return extendChildren;
+        }
+
+        public void setExtendChildren(boolean extendChildren) {
+            this.extendChildren = extendChildren;
+        }
+    }
+}
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index a737bf8..bd239fb 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -51,7 +51,6 @@
         <module>example-ear</module>
         <module>admin-access-app</module>
         <module>angular-product-app</module>
-        <module>angular2-product-app</module>
         <module>database-service</module>
         <module>third-party</module>
         <module>third-party-cdi</module>
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 52b3001..950b226 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -202,28 +202,7 @@ An Angular JS example using Keycloak to secure it.
 If you are already logged in, you will not be asked for a username and password, but you will be redirected to
 an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
 
-Step 10: Angular2 JS Example
-----------------------------------
-An Angular2 JS example using Keycloak to secure it. Angular2 is in beta version yet.
-
-To install angular2
-```
-$ cd keycloak/examples/demo-template/angular2-product-app/src/main/webapp/
-$ npm install
-```
-
-Transpile TypeScript to JavaScript before running the application.
-```
-$ npm run tsc
-```
-
-[http://localhost:8080/angular2-product](http://localhost:8080/angular2-product)
-
-If you are already logged in, you will not be asked for a username and password, but you will be redirected to
-an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
-
-
-Step 11: Pure HTML5/Javascript Example
+Step 10: Pure HTML5/Javascript Example
 ----------------------------------
 An pure HTML5/Javascript example using Keycloak to secure it.
 
@@ -232,7 +211,7 @@ An pure HTML5/Javascript example using Keycloak to secure it.
 If you are already logged in, you will not be asked for a username and password, but you will be redirected to
 an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
 
-Step 12: Service Account Example
+Step 11: Service Account Example
 ================================
 An example for retrieve service account dedicated to the Client Application itself (not to any user). 
 
@@ -240,7 +219,7 @@ An example for retrieve service account dedicated to the Client Application itse
 
 Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
 
-Step 13: Offline Access Example
+Step 12: Offline Access Example
 ===============================
 An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
 is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in 
diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
index e061f5e..75e9d18 100755
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
@@ -156,4 +156,9 @@ public class KerberosFederationProviderFactory implements UserStorageProviderFac
                 AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
     }
 
+    @Override
+    public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
+                AuthenticationExecutionModel.Requirement.DISABLED, null);
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
index c962af8..0d4c07b 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -384,8 +384,14 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
 
     }
 
-
-
+    @Override
+    public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        String allowKerberosCfg = model.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION);
+        if (Boolean.valueOf(allowKerberosCfg)) {
+            CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
+                    AuthenticationExecutionModel.Requirement.DISABLED, null);
+        }
+    }
 
     @Override
     public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
new file mode 100644
index 0000000..1cc51b0
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 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.admin.client.resource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPoliciesResource {
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    Response create(GroupPolicyRepresentation representation);
+
+    @Path("{id}")
+    GroupPolicyResource findById(@PathParam("id") String id);
+
+    @Path("/search")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation findByName(@QueryParam("name") String name);
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
new file mode 100644
index 0000000..6171868
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 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.admin.client.resource;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPolicyResource {
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation toRepresentation();
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    void update(GroupPolicyRepresentation representation);
+
+    @DELETE
+    void remove();
+
+    @Path("/associatedPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> associatedPolicies();
+
+    @Path("/dependentPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> dependentPolicies();
+
+    @Path("/resources")
+    @GET
+    @Produces("application/json")
+    @NoCache
+    List<ResourceRepresentation> resources();
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
index a0af5d4..9ced12c 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
@@ -89,4 +89,7 @@ public interface PoliciesResource {
 
     @Path("client")
     ClientPoliciesResource client();
+
+    @Path("group")
+    GroupPoliciesResource group();
 }
diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md
index 4146eaa..3313f3f 100644
--- a/misc/CrossDataCenter.md
+++ b/misc/CrossDataCenter.md
@@ -3,6 +3,8 @@ Test Cross-Data-Center scenario (test with external JDG server)
 
 These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented. 
 
+Note that these steps are already automated, see Cross-DC tests section in [HOW-TO-RUN.md](../testsuite/integration-arquillian/HOW-TO-RUN.md) document.
+ 
 What is working right now is:
 - Propagating of invalidation messages for "realms" and "users" caches
 - All the other things provided by ClusterProvider, which is:
@@ -18,7 +20,7 @@ Basic setup
 
 This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
 to external JDG server.
- 
+
 JDG Server setup
 ----------------
 - Download JDG 7.0 server and unzip to some folder
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 17ae121..53e496f 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -154,9 +154,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
 
         if (clustered) {
             String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
-            configureTransport(gcb, nodeName);
+            String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
+            configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
+            gcb.globalJmxStatistics()
+              .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
         }
-        gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+        gcb.globalJmxStatistics()
+          .allowDuplicateDomains(allowDuplicateJMXDomains)
+          .enable();
 
         cacheManager = new DefaultCacheManager(gcb.build());
         containerManaged = false;
@@ -317,24 +322,45 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         return cb.build();
     }
 
-    protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) {
+    private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object();
+
+    protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) {
         if (nodeName == null) {
             gcb.transport().defaultTransport();
         } else {
             FileLookup fileLookup = FileLookupFactory.newInstance();
 
-            try {
-                // Compatibility with Wildfly
-                JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
-                channel.setName(nodeName);
-                JGroupsTransport transport = new JGroupsTransport(channel);
-
-                gcb.transport().nodeName(nodeName);
-                gcb.transport().transport(transport);
-
-                logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
-            } catch (Exception e) {
-                throw new RuntimeException(e);
+            synchronized (CHANNEL_INIT_SYNCHRONIZER) {
+                String originalMcastAddr = System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
+                if (jgroupsUdpMcastAddr == null) {
+                    System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
+                } else {
+                    System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, jgroupsUdpMcastAddr);
+                }
+                try {
+                    // Compatibility with Wildfly
+                    JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
+                    channel.setName(nodeName);
+                    JGroupsTransport transport = new JGroupsTransport(channel);
+
+                    gcb.transport()
+                      .nodeName(nodeName)
+                      .transport(transport)
+                      .globalJmxStatistics()
+                        .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
+                        .enable()
+                      ;
+
+                    logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                } finally {
+                    if (originalMcastAddr == null) {
+                        System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
+                    } else {
+                        System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, originalMcastAddr);
+                    }
+                }
             }
         }
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 8618a69..e8cdbf6 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -53,7 +53,9 @@ public interface InfinispanConnectionProvider extends Provider {
 
     // System property used on Wildfly to identify distributedCache address and sticky session route
     String JBOSS_NODE_NAME = "jboss.node.name";
+    String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
 
+    String JMX_DOMAIN = "jboss.datagrid-infinispan";
 
     <K, V> Cache<K, V> getCache(String name);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyRemovedEvent.java
index 759c284..4d46f81 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyRemovedEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyRemovedEvent.java
@@ -30,13 +30,17 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
     private String id;
     private String name;
     private Set<String> resources;
+    private Set<String> resourceTypes;
+    private Set<String> scopes;
     private String serverId;
 
-    public static PolicyRemovedEvent create(String id, String name, Set<String> resources, String serverId) {
+    public static PolicyRemovedEvent create(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId) {
         PolicyRemovedEvent event = new PolicyRemovedEvent();
         event.id = id;
         event.name = name;
         event.resources = resources;
+        event.resourceTypes = resourceTypes;
+        event.scopes = scopes;
         event.serverId = serverId;
         return event;
     }
@@ -53,6 +57,6 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
 
     @Override
     public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
-        cache.policyRemoval(id, name, resources, serverId, invalidations);
+        cache.policyRemoval(id, name, resources, resourceTypes, scopes, serverId, invalidations);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyUpdatedEvent.java
index b576bda..d613c57 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyUpdatedEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PolicyUpdatedEvent.java
@@ -30,13 +30,17 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
     private String id;
     private String name;
     private static Set<String> resources;
+    private Set<String> resourceTypes;
+    private Set<String> scopes;
     private String serverId;
 
-    public static PolicyUpdatedEvent create(String id, String name, Set<String> resources, String serverId) {
+    public static PolicyUpdatedEvent create(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId) {
         PolicyUpdatedEvent event = new PolicyUpdatedEvent();
         event.id = id;
         event.name = name;
         event.resources = resources;
+        event.resourceTypes = resourceTypes;
+        event.scopes = scopes;
         event.serverId = serverId;
         return event;
     }
@@ -53,6 +57,6 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
 
     @Override
     public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
-        cache.policyUpdated(id, name, resources, serverId, invalidations);
+        cache.policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
index c5dc7fc..c74e4fe 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
@@ -17,21 +17,19 @@
 
 package org.keycloak.models.cache.infinispan.authorization;
 
+import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS;
+
 import org.infinispan.Cache;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
-import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.cluster.ClusterEvent;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.cache.CacheRealmProvider;
-import org.keycloak.models.cache.CacheRealmProviderFactory;
 import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
 import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
-import org.keycloak.models.cache.infinispan.RealmCacheManager;
-import org.keycloak.models.cache.infinispan.RealmCacheSession;
+import org.keycloak.models.cache.infinispan.ClearCacheEvent;
 import org.keycloak.models.cache.infinispan.entities.Revisioned;
 import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 
@@ -59,21 +57,17 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
                     Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
                     Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME);
                     storeCache = new StoreFactoryCacheManager(cache, revisions);
-
                     ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-                    cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
 
+                    cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
                         if (event instanceof InvalidationEvent) {
                             InvalidationEvent invalidationEvent = (InvalidationEvent) event;
                             storeCache.invalidationEventReceived(invalidationEvent);
                         }
                     });
 
-                    cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
-
-                        storeCache.clear();
-
-                    });
+                    cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
+                    cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
 
                     log.debug("Registered cluster listeners");
                 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
index 970d1b9..7660c96 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
@@ -47,7 +47,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
     @Override
     public Policy getDelegateForUpdate() {
         if (updated == null) {
-            cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getResourceServerId());
+            cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getResourceServerId());
             updated = cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
@@ -96,7 +96,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
     @Override
     public void setName(String name) {
         getDelegateForUpdate();
-        cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getResourceServerId());
+        cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getScopesIds(), cached.getResourceServerId());
         updated.setName(name);
 
     }
@@ -235,7 +235,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
         getDelegateForUpdate();
         HashSet<String> resources = new HashSet<>();
         resources.add(resource.getId());
-        cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
+        cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getResourceServerId());
         updated.addResource(resource);
 
     }
@@ -245,7 +245,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
         getDelegateForUpdate();
         HashSet<String> resources = new HashSet<>();
         resources.add(resource.getId());
-        cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
+        cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getResourceServerId());
         updated.removeResource(resource);
 
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
index 7943589..63eb8a7 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
@@ -102,7 +102,7 @@ public class StoreFactoryCacheManager extends CacheManager {
         addInvalidations(InResourcePredicate.create().resource(id), invalidations);
     }
 
-    public void policyUpdated(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
+    public void policyUpdated(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId, Set<String> invalidations) {
         invalidations.add(id);
         invalidations.add(StoreFactoryCacheSession.getPolicyByNameCacheKey(name, serverId));
 
@@ -111,10 +111,22 @@ public class StoreFactoryCacheManager extends CacheManager {
                 invalidations.add(StoreFactoryCacheSession.getPolicyByResource(resource, serverId));
             }
         }
+
+        if (resourceTypes != null) {
+            for (String type : resourceTypes) {
+                invalidations.add(StoreFactoryCacheSession.getPolicyByResourceType(type, serverId));
+            }
+        }
+
+        if (scopes != null) {
+            for (String scope : scopes) {
+                invalidations.add(StoreFactoryCacheSession.getPolicyByScope(scope, serverId));
+            }
+        }
     }
 
-    public void policyRemoval(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
-        policyUpdated(id, name, resources, serverId, invalidations);
+    public void policyRemoval(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId, Set<String> invalidations) {
+        policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
     }
 
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index 3e4c205..10be78d 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Supplier;
@@ -252,12 +253,30 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
         invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uri, scopes, serverId));
     }
 
-    public void registerPolicyInvalidation(String id, String name, Set<String> resources, String serverId) {
-        cache.policyUpdated(id, name, resources, serverId, invalidations);
+    public void registerPolicyInvalidation(String id, String name, Set<String> resources, Set<String> scopes, String serverId) {
+        Set<String> resourceTypes = getResourceTypes(resources, serverId);
+        cache.policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
         PolicyAdapter adapter = managedPolicies.get(id);
         if (adapter != null) adapter.invalidateFlag();
 
-        invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, serverId));
+        invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, resourceTypes, scopes, serverId));
+    }
+
+    private Set<String> getResourceTypes(Set<String> resources, String serverId) {
+        if (resources == null) {
+            return Collections.emptySet();
+        }
+
+        return resources.stream().map(resourceId -> {
+            Resource resource = getResourceStore().findById(resourceId, serverId);
+            String type = resource.getType();
+
+            if (type != null) {
+                return type;
+            }
+
+            return null;
+        }).filter(Objects::nonNull).collect(Collectors.toSet());
     }
 
     public ResourceServerStore getResourceServerStoreDelegate() {
@@ -626,7 +645,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
         @Override
         public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
             Policy policy = getPolicyStoreDelegate().create(representation, resourceServer);
-            registerPolicyInvalidation(policy.getId(), policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), resourceServer.getId());
+            registerPolicyInvalidation(policy.getId(), representation.getName(), representation.getResources(), representation.getScopes(), resourceServer.getId());
             return policy;
         }
 
@@ -637,8 +656,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
             if (policy == null) return;
 
             cache.invalidateObject(id);
-            invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId()));
-            cache.policyRemoval(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId(), invalidations);
+            Set<String> resources = policy.getResources().stream().map(resource -> resource.getId()).collect(Collectors.toSet());
+            ResourceServer resourceServer = policy.getResourceServer();
+            Set<String> resourceTypes = getResourceTypes(resources, resourceServer.getId());
+            Set<String> scopes = policy.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet());
+            invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), resources, resourceTypes, scopes, resourceServer.getId()));
+            cache.policyRemoval(id, policy.getName(), resources, resourceTypes, scopes, resourceServer.getId(), invalidations);
             getPolicyStoreDelegate().delete(id);
 
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
index c9832ff..4480f7a 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
@@ -220,7 +220,7 @@ public abstract class CacheManager {
 
         addInvalidationsFromEvent(event, invalidations);
 
-        getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
+        getLogger().debugf("[%s] Invalidating %d cache items after received event %s", cache.getCacheManager().getAddress(), invalidations.size(), event);
 
         for (String invalidation : invalidations) {
             invalidateObject(invalidation);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 6c65423..f9bf0d7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -19,6 +19,8 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.*;
 import org.keycloak.models.cache.CacheRealmProvider;
@@ -129,9 +131,8 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public void clear() {
-        cache.clear();
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-        cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
+        cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false);
     }
 
     @Override
@@ -1076,4 +1077,34 @@ public class RealmCacheSession implements CacheRealmProvider {
         return adapter;
     }
 
+    // Don't cache ClientInitialAccessModel for now
+    @Override
+    public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
+        return getDelegate().createClientInitialAccessModel(realm, expiration, count);
+    }
+
+    @Override
+    public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
+        return getDelegate().getClientInitialAccessModel(realm, id);
+    }
+
+    @Override
+    public void removeClientInitialAccessModel(RealmModel realm, String id) {
+        getDelegate().removeClientInitialAccessModel(realm, id);
+    }
+
+    @Override
+    public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+        return getDelegate().listClientInitialAccess(realm);
+    }
+
+    @Override
+    public void removeExpiredClientInitialAccess() {
+        getDelegate().removeExpiredClientInitialAccess();
+    }
+
+    @Override
+    public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
+        getDelegate().decreaseRemainingCount(realm, clientInitialAccess);
+    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
index 173c434..24742e5 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
@@ -81,6 +81,11 @@ public class ActionTokenReducedKey implements Serializable {
           && Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce());
     }
 
+    @Override
+    public String toString() {
+        return "userId=" + userId + ", actionId=" + actionId + ", actionVerificationNonce=" + actionVerificationNonce;
+    }
+
     public static class ExternalizerImpl implements Externalizer<ActionTokenReducedKey> {
 
         @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java
index 7c0f663..7e3c76e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java
@@ -53,7 +53,7 @@ public class ActionTokenValueEntity implements ActionTokenValueModel {
         public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException {
             output.writeByte(VERSION_1);
 
-            output.writeBoolean(! t.notes.isEmpty());
+            output.writeBoolean(t.notes.isEmpty());
             if (! t.notes.isEmpty()) {
                 output.writeObject(t.notes);
             }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
index 127879a..f02fb5a 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
@@ -19,8 +19,8 @@ package org.keycloak.models.sessions.infinispan;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.models.*;
 
-import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
-import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
+import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent;
+import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent;
 import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
 import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
 import java.util.*;
@@ -58,7 +58,12 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
         ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes);
 
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-        this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue), false);
+        AddInvalidatedActionTokenEvent event = new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue);
+        this.tx.notify(cluster, generateActionTokenEventId(), event, false);
+    }
+
+    private static String generateActionTokenEventId() {
+        return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID();
     }
 
     @Override
@@ -93,6 +98,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
         }
 
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
-        this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false);
+        this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
index a8c5e38..f67f28f 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
@@ -23,14 +23,16 @@ import org.keycloak.common.util.Time;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.*;
 
-import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
-import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
+import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent;
+import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent;
 import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
 import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import org.infinispan.Cache;
 import org.infinispan.context.Flag;
+import org.infinispan.remoting.transport.Address;
+import org.jboss.logging.Logger;
 
 /**
  *
@@ -38,6 +40,10 @@ import org.infinispan.context.Flag;
  */
 public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory {
 
+    private static final Logger LOG = Logger.getLogger(InfinispanActionTokenStoreProviderFactory.class);
+
+    private volatile Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionTokenCache;
+
     public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS";
 
     /**
@@ -49,43 +55,65 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
 
     @Override
     public ActionTokenStoreProvider create(KeycloakSession session) {
+        return new InfinispanActionTokenStoreProvider(session, this.actionTokenCache);
+    }
+
+    @Override
+    public void init(Scope config) {
+        this.config = config;
+    }
+
+    private static Cache<ActionTokenReducedKey, ActionTokenValueEntity> initActionTokenCache(KeycloakSession session) {
         InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
-        Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionTokenCache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
+        Cache<ActionTokenReducedKey, ActionTokenValueEntity> cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
+        final Address cacheAddress = cache.getCacheManager().getAddress();
 
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
 
-        cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
+        cluster.registerListener(ClusterProvider.ALL, event -> {
             if (event instanceof RemoveActionTokensSpecificEvent) {
                 RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
 
-                actionTokenCache
+                LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
+
+                cache
                   .getAdvancedCache()
                   .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD)
                   .keySet()
                   .stream()
                   .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
-                  .forEach(actionTokenCache::remove);
+                  .forEach(cache::remove);
             } else if (event instanceof AddInvalidatedActionTokenEvent) {
                 AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event;
 
+                LOG.debugf("[%s] Invalidating token %s", cacheAddress, e.getKey());
                 if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) {
-                    actionTokenCache.put(e.getKey(), e.getTokenValue());
+                    cache.put(e.getKey(), e.getTokenValue());
                 } else {
-                    actionTokenCache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS);
+                    cache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS);
                 }
             }
         });
 
-        return new InfinispanActionTokenStoreProvider(session, actionTokenCache);
-    }
+        LOG.debugf("[%s] Registered cluster listeners", cacheAddress);
 
-    @Override
-    public void init(Scope config) {
-        this.config = config;
+        return cache;
     }
 
     @Override
     public void postInit(KeycloakSessionFactory factory) {
+        Cache<ActionTokenReducedKey, ActionTokenValueEntity> cache = this.actionTokenCache;
+
+        // It is necessary to put the cache initialization here, otherwise the cache would be initialized lazily, that
+        // means also listeners will start only after first cache initialization - that would be too late
+        if (cache == null) {
+            synchronized (this) {
+                cache = this.actionTokenCache;
+                if (cache == null) {
+                    this.actionTokenCache = initActionTokenCache(factory.create());
+                }
+            }
+        }
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index 83e970d..a9589cc 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -92,7 +92,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
                     ClusterProvider cluster = session.getProvider(ClusterProvider.class);
                     cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
 
-                    log.debug("Registered cluster listeners");
+                    log.debugf("[%s] Registered cluster listeners", authSessionsCache.getCacheManager().getAddress());
                 }
             }
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 0476698..202a051 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -22,7 +22,6 @@ import org.infinispan.CacheStream;
 import org.infinispan.context.Flag;
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.Time;
-import org.keycloak.models.ClientInitialAccessModel;
 import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
@@ -32,23 +31,19 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.session.UserSessionPersisterProvider;
-import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
-import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate;
 import org.keycloak.models.sessions.infinispan.stream.Comparators;
 import org.keycloak.models.sessions.infinispan.stream.Mappers;
 import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
 import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
 import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
-import org.keycloak.models.utils.KeycloakModelUtils;
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -271,7 +266,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         log.debugf("Removing expired sessions");
         removeExpiredUserSessions(realm);
         removeExpiredOfflineUserSessions(realm);
-        removeExpiredClientInitialAccess(realm);
     }
 
     private void removeExpiredUserSessions(RealmModel realm) {
@@ -317,14 +311,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
     }
 
-    private void removeExpiredClientInitialAccess(RealmModel realm) {
-        Iterator<String> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
-                .entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
-        while (itr.hasNext()) {
-            tx.remove(sessionCache, itr.next());
-        }
-    }
-
     @Override
     public void removeUserSessions(RealmModel realm) {
         removeUserSessions(realm, false);
@@ -417,19 +403,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return models;
     }
 
-    List<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
-        List<ClientInitialAccessModel> models = new LinkedList<>();
-        for (ClientInitialAccessEntity e : entities) {
-            models.add(wrap(realm, e));
-        }
-        return models;
-    }
-
-    ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
-        Cache<String, SessionEntity> cache = getCache(false);
-        return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
-    }
-
     UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
         return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
     }
@@ -565,48 +538,4 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache());
     }
 
-    @Override
-    public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
-        String id = KeycloakModelUtils.generateId();
-
-        ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
-        entity.setId(id);
-        entity.setRealm(realm.getId());
-        entity.setTimestamp(Time.currentTime());
-        entity.setExpiration(expiration);
-        entity.setCount(count);
-        entity.setRemainingCount(count);
-
-        tx.put(sessionCache, id, entity);
-
-        return wrap(realm, entity);
-    }
-
-    @Override
-    public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
-        Cache<String, SessionEntity> cache = getCache(false);
-        ClientInitialAccessEntity entity = (ClientInitialAccessEntity) tx.get(cache, id); // Chance created in this transaction
-
-        if (entity == null) {
-            entity = (ClientInitialAccessEntity) cache.get(id);
-        }
-
-        return wrap(realm, entity);
-    }
-
-    @Override
-    public void removeClientInitialAccessModel(RealmModel realm, String id) {
-        tx.remove(getCache(false), id);
-    }
-
-    @Override
-    public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
-        Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId())).iterator();
-        List<ClientInitialAccessModel> list = new LinkedList<>();
-        while (itr.hasNext()) {
-            list.add(wrap(realm, (ClientInitialAccessEntity) itr.next().getValue()));
-        }
-        return list;
-    }
-
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientInitialAccessEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientInitialAccessEntity.java
new file mode 100644
index 0000000..bdb510e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientInitialAccessEntity.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 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.models.jpa.entities;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Entity
+@Table(name="CLIENT_INITIAL_ACCESS")
+@NamedQueries({
+        @NamedQuery(name="findClientInitialAccessByRealm", query="select ia from ClientInitialAccessEntity ia where ia.realm = :realm order by timestamp"),
+        @NamedQuery(name="removeClientInitialAccessByRealm", query="delete from ClientInitialAccessEntity ia where ia.realm = :realm"),
+        @NamedQuery(name="removeExpiredClientInitialAccess", query="delete from ClientInitialAccessEntity ia where (ia.expiration > 0 and (ia.timestamp + ia.expiration) < :currentTime) or ia.remainingCount = 0"),
+        @NamedQuery(name="decreaseClientInitialAccessRemainingCount", query="update ClientInitialAccessEntity ia set ia.remainingCount = ia.remainingCount - 1 where ia.id = :id")
+})
+public class ClientInitialAccessEntity {
+
+    @Id
+    @Column(name="ID", length = 36)
+    @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity.  This avoids an extra SQL
+    protected String id;
+
+    @Column(name="TIMESTAMP")
+    private int timestamp;
+
+    @Column(name="EXPIRATION")
+    private int expiration;
+
+    @Column(name="COUNT")
+    private int count;
+
+    @Column(name="REMAINING_COUNT")
+    private int remainingCount;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "REALM_ID")
+    protected RealmEntity realm;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public int getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(int timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public int getExpiration() {
+        return expiration;
+    }
+
+    public void setExpiration(int expiration) {
+        this.expiration = expiration;
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+    public void setCount(int count) {
+        this.count = count;
+    }
+
+    public int getRemainingCount() {
+        return remainingCount;
+    }
+
+    public void setRemainingCount(int remainingCount) {
+        this.remainingCount = remainingCount;
+    }
+
+    public RealmEntity getRealm() {
+        return realm;
+    }
+
+    public void setRealm(RealmEntity realm) {
+        this.realm = realm;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null) return false;
+        if (!(o instanceof ClientInitialAccessEntity)) return false;
+
+        ClientInitialAccessEntity that = (ClientInitialAccessEntity) o;
+
+        if (!id.equals(that.id)) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index 306f674..c7dea5a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -18,10 +18,24 @@
 package org.keycloak.models.jpa;
 
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
 import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.migration.MigrationModel;
-import org.keycloak.models.*;
-import org.keycloak.models.jpa.entities.*;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientInitialAccessEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
+import org.keycloak.models.jpa.entities.GroupEntity;
+import org.keycloak.models.jpa.entities.RealmEntity;
+import org.keycloak.models.jpa.entities.RoleEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import javax.persistence.EntityManager;
@@ -136,6 +150,8 @@ public class JpaRealmProvider implements RealmProvider {
             removeRole(adapter, role);
         }
 
+        num = em.createNamedQuery("removeClientInitialAccessByRealm")
+                .setParameter("realm", realm).executeUpdate();
 
         em.remove(realm);
 
@@ -557,4 +573,82 @@ public class JpaRealmProvider implements RealmProvider {
         }
         return Collections.unmodifiableList(list);
     }
+
+    @Override
+    public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
+        RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
+
+        ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
+        entity.setId(KeycloakModelUtils.generateId());
+        entity.setRealm(realmEntity);
+
+        entity.setCount(count);
+        entity.setRemainingCount(count);
+
+        int currentTime = Time.currentTime();
+        entity.setTimestamp(currentTime);
+        entity.setExpiration(expiration);
+
+        em.persist(entity);
+
+        return entityToModel(entity);
+    }
+
+    @Override
+    public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
+        ClientInitialAccessEntity entity = em.find(ClientInitialAccessEntity.class, id);
+        if (entity == null) {
+            return null;
+        } else {
+            return entityToModel(entity);
+        }
+    }
+
+    @Override
+    public void removeClientInitialAccessModel(RealmModel realm, String id) {
+        ClientInitialAccessEntity entity = em.find(ClientInitialAccessEntity.class, id);
+        if (entity != null) {
+            em.remove(entity);
+            em.flush();
+        }
+    }
+
+    @Override
+    public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+        RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
+
+        TypedQuery<ClientInitialAccessEntity> query = em.createNamedQuery("findClientInitialAccessByRealm", ClientInitialAccessEntity.class);
+        query.setParameter("realm", realmEntity);
+        List<ClientInitialAccessEntity> entities = query.getResultList();
+
+        return entities.stream()
+                .map(entity -> entityToModel(entity))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public void removeExpiredClientInitialAccess() {
+        int currentTime = Time.currentTime();
+
+        em.createNamedQuery("removeExpiredClientInitialAccess")
+                .setParameter("currentTime", currentTime)
+                .executeUpdate();
+    }
+
+    @Override
+    public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
+        em.createNamedQuery("decreaseClientInitialAccessRemainingCount")
+                .setParameter("id", clientInitialAccess.getId())
+                .executeUpdate();
+    }
+
+    private ClientInitialAccessModel entityToModel(ClientInitialAccessEntity entity) {
+        ClientInitialAccessModel model = new ClientInitialAccessModel();
+        model.setId(entity.getId());
+        model.setCount(entity.getCount());
+        model.setRemainingCount(entity.getRemainingCount());
+        model.setExpiration(entity.getExpiration());
+        model.setTimestamp(entity.getTimestamp());
+        return model;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index a217af3..b71a493 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1865,6 +1865,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         ComponentEntity c = em.find(ComponentEntity.class, component.getId());
         if (c == null) return;
         session.users().preRemove(this, component);
+        ComponentUtil.notifyPreRemove(session, this, component);
         removeComponents(component.getId());
         getEntity().getComponents().remove(c);
     }
@@ -1876,7 +1877,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         getEntity().getComponents().stream()
                 .filter(sameParent)
                 .map(this::entityToModel)
-                .forEach(c -> session.users().preRemove(this, c));
+                .forEach((ComponentModel c) -> {
+                    session.users().preRemove(this, c);
+                    ComponentUtil.notifyPreRemove(session, this, c);
+                });
 
         getEntity().getComponents().removeIf(sameParent);
     }
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
index b3872b8..bd55645 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
@@ -22,6 +22,22 @@
         <dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
         <dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
         <addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
+
+        <createTable tableName="CLIENT_INITIAL_ACCESS">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="REALM_ID" type="VARCHAR(36)"/>
+
+            <column name="TIMESTAMP" type="INT"/>
+            <column name="EXPIRATION" type="INT"/>
+            <column name="COUNT" type="INT"/>
+            <column name="REMAINING_COUNT" type="INT"/>
+        </createTable>
+
+        <addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
+        <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
+
     </changeSet>
 
     <changeSet author="glavoie@gmail.com" id="3.2.0.idx">
@@ -158,5 +174,9 @@
         <createIndex indexName="IDX_WEB_ORIG_CLIENT" tableName="WEB_ORIGINS">
             <column name="CLIENT_ID" type="VARCHAR(36)"/>
         </createIndex>
+
+        <createIndex indexName="IDX_CLIENT_INIT_ACC_REALM" tableName="CLIENT_INITIAL_ACCESS">
+            <column name="REALM_ID" type="VARCHAR(36)"/>
+        </createIndex>
      </changeSet>
 </databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index 5d3fa81..f23198c 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -56,6 +56,7 @@
         <class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
         <class>org.keycloak.models.jpa.entities.ClientTemplateEntity</class>
         <class>org.keycloak.models.jpa.entities.TemplateScopeMappingEntity</class>
+        <class>org.keycloak.models.jpa.entities.ClientInitialAccessEntity</class>
 
         <!-- JpaAuditProviders -->
         <class>org.keycloak.events.jpa.EventEntity</class>
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
index b5f2232..f516124 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
@@ -52,6 +52,7 @@ import java.io.InputStream;
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.util.Objects;
 
 /**
  * Utility dealing with DOM
@@ -554,4 +555,33 @@ public class DocumentUtil {
 
         return documentBuilderFactory;
     }
+
+    /**
+     * Get a (direct) child {@linkplain Element} from the parent {@linkplain Element}. 
+     *
+     * @param parent parent element
+     * @param targetNamespace namespace URI
+     * @param targetLocalName local name
+     * @return a child element matching the target namespace and localname, where {@linkplain Element#getParentNode()} is the parent input parameter
+     * @return
+     */
+    
+    public static Element getDirectChildElement(Element parent, String targetNamespace, String targetLocalName) {
+        Node child = parent.getFirstChild();
+        
+        while(child != null) {
+            if(child instanceof Element) {
+                Element childElement = (Element)child;
+                
+                String ns = childElement.getNamespaceURI();
+                String localName = childElement.getLocalName();
+                
+                if(Objects.equals(targetNamespace, ns) && Objects.equals(targetLocalName, localName)) {
+                    return childElement;
+                }
+            }
+            child = child.getNextSibling();
+        }
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
index ef3e3bd..09ce9d6 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
@@ -49,8 +49,6 @@ public class SAML2Signature {
 
     private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
 
-    private static final String ID_ATTRIBUTE_NAME = "ID";
-
     private String signatureMethod = SignatureMethod.RSA_SHA1;
 
     private String digestMethod = DigestMethod.SHA1;
@@ -156,7 +154,7 @@ public class SAML2Signature {
      */
     public void signSAMLDocument(Document samlDocument, String keyName, KeyPair keypair, String canonicalizationMethodType) throws ProcessingException {
         // Get the ID from the root
-        String id = samlDocument.getDocumentElement().getAttribute(ID_ATTRIBUTE_NAME);
+        String id = samlDocument.getDocumentElement().getAttribute(JBossSAMLConstants.ID.get());
         try {
             sign(samlDocument, id, keyName, keypair, canonicalizationMethodType);
         } catch (ParserConfigurationException | GeneralSecurityException | MarshalException | XMLSignatureException e) {
@@ -210,18 +208,20 @@ public class SAML2Signature {
      *
      * @param document SAML document to have its ID attribute configured.
      */
-    private void configureIdAttribute(Document document) {
+    public static void configureIdAttribute(Document document) {
         // Estabilish the IDness of the ID attribute.
-        document.getDocumentElement().setIdAttribute(ID_ATTRIBUTE_NAME, true);
+        configureIdAttribute(document.getDocumentElement());
 
         NodeList nodes = document.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
                 JBossSAMLConstants.ASSERTION.get());
 
         for (int i = 0; i < nodes.getLength(); i++) {
-            Node n = nodes.item(i);
-            if (n instanceof Element) {
-                ((Element) n).setIdAttribute(ID_ATTRIBUTE_NAME, true);
-            }
+            configureIdAttribute((Element) nodes.item(i));
         }
     }
+    
+    public static void configureIdAttribute(Element element) {
+        element.setIdAttribute(JBossSAMLConstants.ID.get(), true);
+    }
+
 }
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index bb15d23..244fb7d 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -49,11 +49,12 @@ import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
 import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
 import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
 import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
-
+import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
+import javax.xml.crypto.dsig.XMLSignature;
 import javax.xml.datatype.XMLGregorianCalendar;
 import javax.xml.namespace.QName;
 import java.io.ByteArrayInputStream;
@@ -267,42 +268,56 @@ public class AssertionUtil {
     }
 
     /**
-     * Given an assertion element, validate the signature
+     * Given an {@linkplain Element}, validate the Signature direct child element
      *
-     * @param assertionElement
+     * @param element parent {@linkplain Element}
      * @param publicKey the {@link PublicKey}
      *
-     * @return
+     * @return true if signature is present and valid
      */
-    public static boolean isSignatureValid(Element assertionElement, PublicKey publicKey) {
-        try {
-            Document doc = DocumentUtil.createDocument();
-            Node n = doc.importNode(assertionElement, true);
-            doc.appendChild(n);
-
-            return new SAML2Signature().validate(doc, new HardcodedKeyLocator(publicKey));
-        } catch (Exception e) {
-            logger.signatureAssertionValidationError(e);
-        }
-        return false;
+    public static boolean isSignatureValid(Element element, PublicKey publicKey) {
+        return isSignatureValid(element, new HardcodedKeyLocator(publicKey));
     }
 
     /**
-     * Given an assertion element, validate the signature.
+     * Given an {@linkplain Element}, validate the Signature direct child element
+     *
+     * @param element parent {@linkplain Element}
+     * @param keyLocator the {@link KeyLocator}
+     *
+     * @return true if signature is present and valid
      */
-    public static boolean isSignatureValid(Element assertionElement, KeyLocator keyLocator) {
+    
+    public static boolean isSignatureValid(Element element, KeyLocator keyLocator) {
         try {
-            Document doc = DocumentUtil.createDocument();
-            Node n = doc.importNode(assertionElement, true);
-            doc.appendChild(n);
-
-            return new SAML2Signature().validate(doc, keyLocator);
+            SAML2Signature.configureIdAttribute(element);
+            
+            Element signature = getSignature(element);
+            if(signature != null) {
+                return XMLSignatureUtil.validateSingleNode(signature, keyLocator);
+            }
         } catch (Exception e) {
             logger.signatureAssertionValidationError(e);
         }
         return false;
     }
+    
+    /**
+     * 
+     * Given an {@linkplain Element}, check if there is a Signature direct child element
+     * 
+     * @param element parent {@linkplain Element}
+     * @return true if signature is present
+     */
 
+    public static boolean isSignedElement(Element element) {
+        return getSignature(element) != null;
+    }
+    
+    protected static Element getSignature(Element element) {
+        return DocumentUtil.getDirectChildElement(element, XMLSignature.XMLNS, "Signature");
+    }
+    
     /**
      * Check whether the assertion has expired
      *
@@ -570,8 +585,8 @@ public class AssertionUtil {
 
     /**
      * This method modifies the given responseType, and replaces the encrypted assertion with a decrypted version.
-     *
-     * It returns the assertion element as it was decrypted. This can be used in sginature verification.
+     * @param responseType a response containg an encrypted assertion
+     * @return the assertion element as it was decrypted. This can be used in signature verification.
      */
     public static Element decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException {
         SAML2Response saml2Response = new SAML2Response();
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
index 53228c9..7093a20 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
@@ -468,7 +468,7 @@ public class XMLSignatureUtil {
         return true;
     }
 
-    private static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
+    public static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
         KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
         try {
             if (validateUsingKeySelector(signatureNode, sel)) {
diff --git a/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java b/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java
new file mode 100644
index 0000000..b60bb4b
--- /dev/null
+++ b/saml-core/src/test/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtilTest.java
@@ -0,0 +1,58 @@
+package org.keycloak.saml.processing.core.saml.v2.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.X509Certificate;
+
+import org.bouncycastle.util.Arrays;
+import org.junit.Test;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.DerUtils;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public class AssertionUtilTest {
+
+    private static final String PRIVATE_KEY = "MIICWwIBAAKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3og0RdSbgIxAsIpEa1FUwU2W5yvI6mNeJ3ibFgCgcxqPk6GkAC7DWfQfdQ8cS+dCuaFTs8ObIQEvU50YzeNPiiFxRA+MnauCUXaKm/PnDfjd4tPgru7XZvlGh0wECQQDsBbN2cKkBKpr/b5oJiBcBaSZtWiMNuYBDn9x8uORj+Gy/49BUIMHF2EWyxOWz6ocP5YiynNRkPe21Zus7PEr1AkEA5yWQOkxUTIg43s4pxNSeHtL+Ebqcg54lY2xOQK0yufxUVZI8ODctAKmVBMiCKpU3mZQquOaQicuGtocpgxlScQI/YM31zZ5nsxLGf/5GL6KhzPJT0IYn2nk7IoFu7bjn9BjwgcPurpLA52TNMYWQsTqAKwT6DEhG1NaRqNWNpb4VAkBehObAYBwMm5udyHIeEc+CzUalm0iLLa0eRdiN7AUVNpCJ2V2Uo0NcxPux1AgeP5xXydXafDXYkwhINWcNO9qRAkEA58ckAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZV9wN85V0xZXWsw==";
+
+    /**
+     * The public certificate that corresponds to {@link #PRIVATE_KEY}.
+     */
+    private static final String PUBLIC_CERT = "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE1MDEyODIyMTYyMFoXDTE3MTAyNDIyMTYyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAII/K9NNvXi9IySl7+l2zY/kKrGTtuR4WdCI0xLW/Jn4dLY7v1/HOnV4CC4ecFOzhdNFPtJkmEhP/q62CpmOYOKApXk3tfmm2rwEz9bWprVxgFGKnbrWlz61Z/cjLAlhD3IUj2ZRBquYgSXQPsYfXo1JmSWF5pZ9uh1FVqu9f4wvRqY20ZhUN+39F+1iaBsoqsrbXypCn1HgZkW1/9D9GZug1c3vB4wg1TwZZWRNGtxwoEhdK6dPrNcZ+6PdanVilWrbQFbBjY4wz8/7IMBzssoQ7Usmo8F1Piv0FGfaVeJqBrcAvbiBMpk8pT+27u6p8VyIX6LhGvnxIwM07NByeSUCAwEAAaMhMB8wHQYDVR0OBBYEFFlcNuTYwI9W0tQ224K1gFJlMam0MA0GCSqGSIb3DQEBCwUAA4IBAQB5snl1KWOJALtAjLqD0mLPg1iElmZP82Lq1htLBt3XagwzU9CaeVeCQ7lTp+DXWzPa9nCLhsC3QyrV3/+oqNli8C6NpeqI8FqN2yQW/QMWN1m5jWDbmrWwtQzRUn/rh5KEb5m3zPB+tOC6e/2bV3QeQebxeW7lVMD0tSCviUg1MQf1l2gzuXQo60411YwqrXwk6GMkDOhFDQKDlMchO3oRbQkGbcP8UeiKAXjMeHfzbiBr+cWz8NYZEtxUEDYDjTpKrYCSMJBXpmgVJCZ00BswbksxJwaGqGMPpUKmCV671pf3m8nq3xyiHMDGuGwtbU+GE8kVx85menmp8+964nin";
+
+    @Test
+    public void testSaml20Signed() throws Exception {
+        
+        X509Certificate decodeCertificate = DerUtils.decodeCertificate(new ByteArrayInputStream(Base64.decode(PUBLIC_CERT)));
+        
+        try (InputStream st = AssertionUtilTest.class.getResourceAsStream("saml20-signed-response.xml")) {
+            Document document = DocumentUtil.getDocument(st);
+            
+            Element assertion = DocumentUtil.getDirectChildElement(document.getDocumentElement(), "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
+            
+            assertTrue(AssertionUtil.isSignatureValid(assertion, decodeCertificate.getPublicKey()));
+            
+            // test manipulation of signature
+            Element signatureElement = AssertionUtil.getSignature(assertion);
+            byte[] validSignature = Base64.decode(signatureElement.getTextContent());
+            
+            // change the signature value slightly
+            byte[] invalidSignature = Arrays.clone(validSignature);
+            invalidSignature[0] ^= invalidSignature[0];
+            signatureElement.setTextContent(Base64.encodeBytes(invalidSignature));
+            
+            // check that signature now is invalid
+            assertFalse(AssertionUtil.isSignatureValid(document.getDocumentElement(), decodeCertificate.getPublicKey()));
+            
+            // restore valid signature, but remove Signature element, check that still invalid
+            signatureElement.setTextContent(Base64.encodeBytes(validSignature));
+
+            assertion.removeChild(signatureElement);
+            assertFalse(AssertionUtil.isSignatureValid(document.getDocumentElement(), decodeCertificate.getPublicKey()));
+        }
+    }
+
+}
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/saml/v2/util/saml20-signed-response.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/saml/v2/util/saml20-signed-response.xml
new file mode 100644
index 0000000..998520a
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/saml/v2/util/saml20-signed-response.xml
@@ -0,0 +1 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider/endpoint" ID="ID_9aca1381-6265-434c-98a5-d89236d32ea0" InResponseTo="ID_2d81575a-0bde-488a-b69d-0b78c0fcf521" IssueInstant="2017-05-04T22:18:13.550Z" Version="2.0"><saml:Issuer>http://localhost:8080/auth/realms/saml-broker-realm</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="ID_e9d22a60-7954-48b2-ad97-700d70e332f5" IssueInstant="2017-05-04T22:18:13.546Z" Version="2.0"><saml:Issuer>http://localhost:8080/auth/realms/saml-broker-realm</saml:Issuer><dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"><dsig:SignedInfo><dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><dsig:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><dsig:Reference URI="#ID_e9d22a60-7954-48b2-ad97-700d70e332f5"><dsig:Transforms><dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></dsig:Transforms><dsig:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><dsig:DigestValue>cCSNXxLmu411weW1kRpie4C9yaBg2In6V4oEuqya0Eo=</dsig:DigestValue></dsig:Reference></dsig:SignedInfo><dsig:SignatureValue>Qe6ZqgSwFH31UTu+zHqr1/UsafH0luxP5OH/cqyHm07Kf/Fp/fm9mnHJ0kGoUn0SUo7xWvwy8AzUfPXWMYS3kDyhUsPzgz0CnCzzfTz3koKFczgyIQ8sokIDv0cTp3z1qCUVWV0CEPzhtWlaIus2W89TEi/h9KjYrkeGl3+cpm8BPEAt4EP8Oht5czK2haIfPMDUm5Y7uw/FCSsvSfFyrlJ0jR/YMeP9PP0InYYegI9QQgvXKRm6DZSNZgKYFpprc12v6vv/zTaMm5fbuuy1wNDuDTB8EF6K1yrq21DatJXUKE1oOMBrkOvbFJNtgHlQviz1OssAqzHlf0NQPIAEig==</dsig:SignatureValue><dsig:KeyInfo><dsig:KeyName>IzH2UxfMxovYTEHn4Bh-EAj-Zrvldukl_5Snu0RA0B8</dsig:KeyName><dsig:X509Data><dsig:X509Certificate>MIICsTCCAZkCBgFb1GERYzANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFzYW1sLWJyb2tlci1yZWFsbTAeFw0xNzA1MDQxNjUxMjJaFw0yNzA1MDQxNjUzMDJaMBwxGjAYBgNVBAMMEXNhbWwtYnJva2VyLXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF00U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlEGq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmRbX/0P0Zm6DVze8HjCDVPBllZE0a3HCgSF0rp0+s1xn7o91qdWKVattAVsGNjjDPz/sgwHOyyhDtSyajwXU+K/QUZ9pV4moGtwC9uIEymTylP7bu7qnxXIhfouEa+fEjAzTs0HJ5JQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCBuWKmcVPX4sZtpvBp8BgQwH51l5tKPuQq66/4JH40hzkrLOGcVEOafIsGoOWi8HsZWZu+APwhSrnRNd7yMV+NJyw5W1DKjhdUPWnzPNy+UMAcoUBFhXDIWog0qxgGvTdoe1lfHryUQt1cd95SFVIerJA93nFbSOoMB+N7TmfQm+sNu2pJ2tr6mx3wGCXMnWf29gwhCI3wV19hh4KugnMIEStjvQoRyh2yna64BrR3eaUyhU/Bdrq2VXLNU/9WXg9gbRLUEWkMUPKOeQ5cGCgc4JFyFXRo5ExkzmvP9vwBRtjQulk5QKqfYo251mKvTQgO7K8d4CzVS/4+bpgKvZAM</dsig:X509Certificate></dsig:X509Data><dsig:KeyValue><dsig:RSAKeyValue><dsig:Modulus>gj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF00U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlEGq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmRbX/0P0Zm6DVze8HjCDVPBllZE0a3HCgSF0rp0+s1xn7o91qdWKVattAVsGNjjDPz/sgwHOyyhDtSyajwXU+K/QUZ9pV4moGtwC9uIEymTylP7bu7qnxXIhfouEa+fEjAzTs0HJ5JQ==</dsig:Modulus><dsig:Exponent>AQAB</dsig:Exponent></dsig:RSAKeyValue></dsig:KeyValue></dsig:KeyInfo></dsig:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">G-ebb3a66f-686f-4bb9-8a8b-20b566ca747b</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData InResponseTo="ID_2d81575a-0bde-488a-b69d-0b78c0fcf521" NotOnOrAfter="2017-05-04T22:23:11.546Z" Recipient="http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider/endpoint"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2017-05-04T22:18:11.546Z" NotOnOrAfter="2017-05-04T22:19:11.546Z"><saml:AudienceRestriction><saml:Audience>http://localhost:8080/auth/realms/saml-broker-authentication-realm</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2017-05-04T22:18:13.551Z" SessionIndex="c0cc880f-83d7-461b-b299-9fb3354f598c"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="Role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">manager</saml:AttributeValue></saml:Attribute><saml:Attribute Name="Role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
index 695637f..b4c6f44 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
@@ -80,6 +80,18 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
     }
 
     /**
+     * Called before the component is removed.
+     *
+     * @param session
+     * @param realm
+     * @param model model of the component, which is going to be removed
+     */
+    default
+    void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+
+    /**
      * These are config properties that are common across all implementation of this component type
      *
      * @return
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java b/server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
old mode 100755
new mode 100644
index 149b6aa..24b5546
--- a/server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
@@ -20,20 +20,55 @@ package org.keycloak.models;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public interface ClientInitialAccessModel {
+public class ClientInitialAccessModel {
 
-    String getId();
+    private String id;
 
-    RealmModel getRealm();
+    private int timestamp;
 
-    int getTimestamp();
+    private int expiration;
 
-    int getExpiration();
+    private int count;
 
-    int getCount();
+    private int remainingCount;
 
-    int getRemainingCount();
+    public String getId() {
+        return id;
+    }
 
-    void decreaseRemainingCount();
+    public void setId(String id) {
+        this.id = id;
+    }
 
+    public int getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(int timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public int getExpiration() {
+        return expiration;
+    }
+
+    public void setExpiration(int expiration) {
+        this.expiration = expiration;
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+    public void setCount(int count) {
+        this.count = count;
+    }
+
+    public int getRemainingCount() {
+        return remainingCount;
+    }
+
+    public void setRemainingCount(int remainingCount) {
+        this.remainingCount = remainingCount;
+    }
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
index 72c11ad..3b043fc 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
@@ -98,4 +98,11 @@ public interface RealmProvider extends Provider {
     List<RealmModel> getRealms();
     boolean removeRealm(String id);
     void close();
+
+    ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
+    ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
+    void removeClientInitialAccessModel(RealmModel realm, String id);
+    List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
+    void removeExpiredClientInitialAccess();
+    void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index d474e89..848c098 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -72,11 +72,6 @@ public interface UserSessionProvider extends Provider {
     /** Triggered by persister during pre-load. It optionally imports authenticatedClientSessions too if requested. Otherwise the imported UserSession will have empty list of AuthenticationSessionModel **/
     UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline, boolean importAuthenticatedClientSessions);
 
-    ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
-    ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
-    void removeClientInitialAccessModel(RealmModel realm, String id);
-    List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
-
     void close();
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index c83d9f8..0719bab 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -107,6 +107,10 @@ public interface Attributes {
             return values.length;
         }
 
+        public boolean isEmpty() {
+            return values.length == 0;
+        }
+
         public String asString(int idx) {
             if (idx >= values.length) {
                 throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
index 8a55ff1..644b90a 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
@@ -21,14 +21,17 @@ package org.keycloak.authorization;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
 import org.keycloak.authorization.permission.evaluator.Evaluators;
 import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
 import org.keycloak.authorization.policy.provider.PolicyProvider;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
-import org.keycloak.authorization.store.AuthorizationStoreFactory;
 import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.ResourceServerStore;
 import org.keycloak.authorization.store.ResourceStore;
@@ -37,7 +40,6 @@ import org.keycloak.authorization.store.StoreFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
-import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.provider.Provider;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
@@ -143,6 +145,61 @@ public final class AuthorizationProvider implements Provider {
                 return new PolicyStore() {
                     @Override
                     public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
+                        Set<String> resources = representation.getResources();
+
+                        if (resources != null) {
+                            representation.setResources(resources.stream().map(id -> {
+                                Resource resource = getResourceStore().findById(id, resourceServer.getId());
+
+                                if (resource == null) {
+                                    resource = getResourceStore().findByName(id, resourceServer.getId());
+                                }
+
+                                if (resource == null) {
+                                    throw new RuntimeException("Resource [" + id + "] does not exist");
+                                }
+
+                                return resource.getId();
+                            }).collect(Collectors.toSet()));
+                        }
+
+                        Set<String> scopes = representation.getScopes();
+
+                        if (scopes != null) {
+                            representation.setScopes(scopes.stream().map(id -> {
+                                Scope scope = getScopeStore().findById(id, resourceServer.getId());
+
+                                if (scope == null) {
+                                    scope = getScopeStore().findByName(id, resourceServer.getId());
+                                }
+
+                                if (scope == null) {
+                                    throw new RuntimeException("Scope [" + id + "] does not exist");
+                                }
+
+                                return scope.getId();
+                            }).collect(Collectors.toSet()));
+                        }
+
+
+                        Set<String> policies = representation.getPolicies();
+
+                        if (policies != null) {
+                            representation.setPolicies(policies.stream().map(id -> {
+                                Policy policy = getPolicyStore().findById(id, resourceServer.getId());
+
+                                if (policy == null) {
+                                    policy = getPolicyStore().findByName(id, resourceServer.getId());
+                                }
+
+                                if (policy == null) {
+                                    throw new RuntimeException("Policy [" + id + "] does not exist");
+                                }
+
+                                return policy.getId();
+                            }).collect(Collectors.toSet()));
+                        }
+
                         return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer));
                     }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/ResourceType.java b/server-spi-private/src/main/java/org/keycloak/events/admin/ResourceType.java
index cba5ed6..128b344 100644
--- a/server-spi-private/src/main/java/org/keycloak/events/admin/ResourceType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/admin/ResourceType.java
@@ -156,5 +156,25 @@ public enum ResourceType {
     /**
      *
      */
-    , COMPONENT;
+    , COMPONENT
+
+    /**
+     *
+     */
+    , AUTHORIZATION_RESOURCE_SERVER
+
+    /**
+     *
+     */
+    , AUTHORIZATION_RESOURCE
+
+    /**
+     *
+     */
+    , AUTHORIZATION_SCOPE
+
+    /**
+     *
+     */
+    , AUTHORIZATION_POLICY;
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index ac435e3..cb203c1 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -120,4 +120,6 @@ public interface LoginFormsProvider extends Provider {
     public LoginFormsProvider setStatus(Response.Status status);
 
     LoginFormsProvider setActionUri(URI requestUri);
+
+    LoginFormsProvider setExecution(String execution);
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
index 1d41f0d..0c603c4 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models.utils;
 
+import org.jboss.logging.Logger;
 import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.models.KeycloakSession;
@@ -38,6 +39,8 @@ import java.util.Map;
  */
 public class ComponentUtil {
 
+    private static final Logger logger = Logger.getLogger(ComponentUtil.class);
+
     public static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, ComponentRepresentation component) {
         return getComponentConfigProperties(session, component.getProviderType(), component.getProviderId());
     }
@@ -102,5 +105,14 @@ public class ComponentUtil {
             ((OnUpdateComponent)session.userStorageManager()).onUpdate(session, realm, oldModel, newModel);
         }
     }
+    public static void notifyPreRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        try {
+            ComponentFactory factory = getComponentFactory(session, model);
+            factory.preRemove(session, realm, model);
+        } catch (IllegalArgumentException iae) {
+            // We allow to remove broken providers without throwing an exception
+            logger.warn(iae.getMessage());
+        }
+    }
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index ab32a22..f5b48fa 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -738,11 +738,7 @@ public class ModelToRepresentation {
         return rep;
     }
 
-    public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider) {
-        return toRepresentation(model, authorizationProvider, true);
-    }
-
-    public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider, boolean deep) {
+    public static ScopeRepresentation toRepresentation(Scope model) {
         ScopeRepresentation scope = new ScopeRepresentation();
 
         scope.setId(model.getId());
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index e102d3d..4a4b4fb 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -2171,13 +2171,13 @@ public class RepresentationToModel {
     private static void updateResources(Set<String> resourceIds, Policy policy, StoreFactory storeFactory) {
         if (resourceIds != null) {
             if (resourceIds.isEmpty()) {
-                for (Scope scope : new HashSet<Scope>(policy.getScopes())) {
-                    policy.removeScope(scope);
+                for (Resource resource : new HashSet<>(policy.getResources())) {
+                    policy.removeResource(resource);
                 }
             }
             for (String resourceId : resourceIds) {
                 boolean hasResource = false;
-                for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
+                for (Resource resourceModel : new HashSet<>(policy.getResources())) {
                     if (resourceModel.getId().equals(resourceId) || resourceModel.getName().equals(resourceId)) {
                         hasResource = true;
                     }
@@ -2196,7 +2196,7 @@ public class RepresentationToModel {
                 }
             }
 
-            for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
+            for (Resource resourceModel : new HashSet<>(policy.getResources())) {
                 boolean hasResource = false;
 
                 for (String resourceId : resourceIds) {
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
new file mode 100644
index 0000000..2a76add
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
@@ -0,0 +1,14 @@
+package org.keycloak.scripting;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a {@link ScriptModel} so it can be evaluated with custom bindings.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+public interface EvaluatableScriptAdapter {
+    ScriptModel getScriptModel();
+
+    Object eval(ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException;
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
index c3859ab..17bb4a1 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
@@ -56,7 +56,7 @@ public class InvocableScriptAdapter implements Invocable {
         }
 
         this.scriptModel = scriptModel;
-        this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
+        this.scriptEngine = scriptEngine;
     }
 
     @Override
@@ -101,17 +101,6 @@ public class InvocableScriptAdapter implements Invocable {
         return candidate != null;
     }
 
-    private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
-
-        try {
-            engine.eval(script.getCode());
-        } catch (ScriptException se) {
-            throw new ScriptExecutionException(script, se);
-        }
-
-        return engine;
-    }
-
     private Invocable getInvocableEngine() {
         return (Invocable) scriptEngine;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 67bad5a..ef2990f 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -39,6 +39,14 @@ public interface ScriptingProvider extends Provider {
     InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
 
     /**
+     * Returns an {@link EvaluatableScriptAdapter} based on the given {@link ScriptModel}.
+     * <p>The {@code EvaluatableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with empty bindings.</p>
+     *
+     * @param scriptModel the scriptModel to wrap
+     */
+    EvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel);
+
+    /**
      * Creates a new {@link ScriptModel} instance.
      *
      * @param realmId
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 23d06e3..af7d2f7 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -471,6 +471,7 @@ public class AuthenticationProcessor {
             LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
                     .setUser(getUser())
                     .setActionUri(action)
+                    .setExecution(getExecution().getId())
                     .setFormData(request.getDecodedFormParameters())
                     .setClientSessionCode(accessCode);
             if (getForwardedErrorMessage() != null) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index 7189b95..ca841d0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -169,6 +169,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator 
                 .setStatus(Response.Status.OK)
                 .setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
                 .setActionUri(action)
+                .setExecution(context.getExecution().getId())
                 .createIdpLinkEmailPage();
         context.forceChallenge(challenge);
     }
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index 82c12ec..575677d 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -270,6 +270,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
         URI actionUrl = getActionUrl(executionId, code);
         LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class)
                 .setActionUri(actionUrl)
+                .setExecution(executionId)
                 .setClientSessionCode(code)
                 .setFormData(formData)
                 .setErrors(errors);
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 1d9475a..3afb34c 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -137,11 +137,15 @@ public class RequiredActionContextResult implements RequiredActionContext {
         ClientModel client = authenticationSession.getClient();
         return LoginActionsService.requiredActionProcessor(getUriInfo())
                 .queryParam(OAuth2Constants.CODE, code)
-                .queryParam(Constants.EXECUTION, factory.getId())
+                .queryParam(Constants.EXECUTION, getExecution())
                 .queryParam(Constants.CLIENT_ID, client.getClientId())
                 .build(getRealm().getName());
     }
 
+    private String getExecution() {
+        return factory.getId();
+    }
+
     @Override
     public String generateCode() {
         ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
@@ -164,6 +168,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
         LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
                 .setUser(getUser())
                 .setActionUri(action)
+                .setExecution(getExecution())
                 .setClientSessionCode(accessCode);
         return provider;
     }
diff --git a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
index a4ff868..bc9dd1f 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
@@ -23,6 +23,7 @@ import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 
 import javax.ws.rs.Path;
@@ -34,14 +35,14 @@ public class AuthorizationService {
 
     private final RealmAuth auth;
     private final ClientModel client;
-    private final KeycloakSession session;
     private final ResourceServer resourceServer;
     private final AuthorizationProvider authorization;
+    private final AdminEventBuilder adminEvent;
 
-    public AuthorizationService(KeycloakSession session, ClientModel client, RealmAuth auth) {
-        this.session = session;
+    public AuthorizationService(KeycloakSession session, ClientModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.client = client;
         this.authorization = session.getProvider(AuthorizationProvider.class);
+        this.adminEvent = adminEvent;
         this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(this.client.getId());
         this.auth = auth;
 
@@ -52,16 +53,16 @@ public class AuthorizationService {
 
     @Path("/resource-server")
     public ResourceServerService resourceServer() {
-        ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth);
+        ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
         return resource;
     }
 
-    public void enable() {
+    public void enable(boolean newClient) {
         if (!isEnabled()) {
-            resourceServer().create();
+            resourceServer().create(newClient);
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java
index ba4fae2..ea4677d 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java
@@ -22,6 +22,7 @@ import java.util.Map;
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 
 /**
@@ -29,18 +30,18 @@ import org.keycloak.services.resources.admin.RealmAuth;
  */
 public class PermissionService extends PolicyService {
 
-    public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
-        super(resourceServer, authorization, auth);
+    public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
+        super(resourceServer, authorization, auth, adminEvent);
     }
 
     @Override
     protected PolicyResourceService doCreatePolicyResource(Policy policy) {
-        return new PolicyTypeResourceService(policy, resourceServer, authorization, auth);
+        return new PolicyTypeResourceService(policy, resourceServer, authorization, auth, adminEvent);
     }
 
     @Override
     protected PolicyTypeService doCreatePolicyTypeResource(String type) {
-        return new PolicyTypeService(type, resourceServer, authorization, auth) {
+        return new PolicyTypeService(type, resourceServer, authorization, auth, adminEvent) {
             @Override
             protected List<Object> doSearch(Integer firstResult, Integer maxResult, Map<String, String[]> filters) {
                 filters.put("permission", new String[] {Boolean.TRUE.toString()});
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java
index 85e0943..c0728bc 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java
@@ -26,8 +26,10 @@ import javax.ws.rs.GET;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
 
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.keycloak.authorization.AuthorizationProvider;
@@ -36,12 +38,15 @@ import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 import org.keycloak.util.JsonSerialization;
 
@@ -54,19 +59,21 @@ public class PolicyResourceService {
     protected final ResourceServer resourceServer;
     protected final AuthorizationProvider authorization;
     protected final RealmAuth auth;
+    private final AdminEventBuilder adminEvent;
 
-    public PolicyResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+    public PolicyResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.policy = policy;
         this.resourceServer = resourceServer;
         this.authorization = authorization;
         this.auth = auth;
+        this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_POLICY);
     }
 
     @PUT
     @Consumes("application/json")
     @Produces("application/json")
     @NoCache
-    public Response update(String payload) {
+    public Response update(@Context UriInfo uriInfo,  String payload) {
         this.auth.requireManage();
 
         AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
@@ -79,11 +86,14 @@ public class PolicyResourceService {
 
         RepresentationToModel.toModel(representation, authorization, policy);
 
+
+        audit(uriInfo, representation, OperationType.UPDATE);
+
         return Response.status(Status.CREATED).build();
     }
 
     @DELETE
-    public Response delete() {
+    public Response delete(@Context UriInfo uriInfo) {
         this.auth.requireManage();
 
         if (policy == null) {
@@ -98,6 +108,10 @@ public class PolicyResourceService {
 
         policyStore.delete(policy.getId());
 
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            audit(uriInfo, toRepresentation(policy, authorization), OperationType.DELETE);
+        }
+
         return Response.noContent().build();
     }
 
@@ -225,4 +239,10 @@ public class PolicyResourceService {
     protected Policy getPolicy() {
         return policy;
     }
+
+    private void audit(@Context UriInfo uriInfo, AbstractPolicyRepresentation policy, OperationType operation) {
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            adminEvent.operation(operation).resourcePath(uriInfo).representation(policy).success();
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
index e670042..e8d4aa8 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
@@ -33,9 +33,11 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
 
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
@@ -46,12 +48,16 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
 import org.keycloak.models.Constants;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
 import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 import org.keycloak.util.JsonSerialization;
 
@@ -63,11 +69,13 @@ public class PolicyService {
     protected final ResourceServer resourceServer;
     protected final AuthorizationProvider authorization;
     protected final RealmAuth auth;
+    protected final AdminEventBuilder adminEvent;
 
-    public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+    public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.resourceServer = resourceServer;
         this.authorization = authorization;
         this.auth = auth;
+        this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_POLICY);
     }
 
     @Path("{type}")
@@ -84,18 +92,18 @@ public class PolicyService {
     }
 
     protected PolicyTypeService doCreatePolicyTypeResource(String type) {
-        return new PolicyTypeService(type, resourceServer, authorization, auth);
+        return new PolicyTypeService(type, resourceServer, authorization, auth, adminEvent);
     }
 
     protected Object doCreatePolicyResource(Policy policy) {
-        return new PolicyResourceService(policy, resourceServer, authorization, auth);
+        return new PolicyResourceService(policy, resourceServer, authorization, auth, adminEvent);
     }
 
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @NoCache
-    public Response create(String payload) {
+    public Response create(@Context UriInfo uriInfo,  String payload) {
         this.auth.requireManage();
 
         AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
@@ -103,6 +111,8 @@ public class PolicyService {
 
         representation.setId(policy.getId());
 
+        audit(uriInfo, representation, representation.getId(), OperationType.CREATE);
+
         return Response.status(Status.CREATED).entity(representation).build();
     }
 
@@ -280,4 +290,14 @@ public class PolicyService {
             findAssociatedPolicies(associated, policies);
         });
     }
+
+    private void audit(@Context UriInfo uriInfo, AbstractPolicyRepresentation resource, String id, OperationType operation) {
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            if (id != null) {
+                adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();
+            } else {
+                adminEvent.operation(operation).resourcePath(uriInfo).representation(resource).success();
+            }
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java
index 8756721..9044c0d 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java
@@ -24,6 +24,7 @@ import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 import org.keycloak.util.JsonSerialization;
 
@@ -32,8 +33,8 @@ import org.keycloak.util.JsonSerialization;
  */
 public class PolicyTypeResourceService extends PolicyResourceService {
 
-    public PolicyTypeResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
-        super(policy, resourceServer, authorization, auth);
+    public PolicyTypeResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
+        super(policy, resourceServer, authorization, auth, adminEvent);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java
index 5f03dad..6f37f6c 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java
@@ -30,6 +30,7 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 import org.keycloak.util.JsonSerialization;
 
@@ -40,8 +41,8 @@ public class PolicyTypeService extends PolicyService {
 
     private final String type;
 
-    PolicyTypeService(String type, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
-        super(resourceServer, authorization, auth);
+    PolicyTypeService(String type, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
+        super(resourceServer, authorization, auth, adminEvent);
         this.type = type;
     }
 
@@ -60,7 +61,7 @@ public class PolicyTypeService extends PolicyService {
 
     @Override
     protected Object doCreatePolicyResource(Policy policy) {
-        return new PolicyTypeResourceService(policy, resourceServer,authorization, auth);
+        return new PolicyTypeResourceService(policy, resourceServer,authorization, auth, adminEvent);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
index 15f1db7..777d843 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
@@ -40,12 +40,15 @@ import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.ResourceStore;
 import org.keycloak.authorization.store.ScopeStore;
 import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
 import org.keycloak.exportimport.util.ExportUtils;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.representations.idm.authorization.DecisionStrategy;
 import org.keycloak.representations.idm.authorization.Logic;
@@ -53,6 +56,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 
 /**
@@ -62,19 +66,24 @@ public class ResourceServerService {
 
     private final AuthorizationProvider authorization;
     private final RealmAuth auth;
+    private final AdminEventBuilder adminEvent;
     private final KeycloakSession session;
     private ResourceServer resourceServer;
     private final ClientModel client;
 
-    public ResourceServerService(AuthorizationProvider authorization, ResourceServer resourceServer, ClientModel client, RealmAuth auth) {
+    @Context
+    private UriInfo uriInfo;
+
+    public ResourceServerService(AuthorizationProvider authorization, ResourceServer resourceServer, ClientModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.authorization = authorization;
         this.session = authorization.getKeycloakSession();
         this.client = client;
         this.resourceServer = resourceServer;
         this.auth = auth;
+        this.adminEvent = adminEvent;
     }
 
-    public void create() {
+    public void create(boolean newClient) {
         this.auth.requireManage();
 
         UserModel serviceAccount = this.session.users().getServiceAccount(client);
@@ -86,16 +95,17 @@ public class ResourceServerService {
         this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().create(this.client.getId());
         createDefaultRoles(serviceAccount);
         createDefaultPermission(createDefaultResource(), createDefaultPolicy());
+        audit(OperationType.CREATE, uriInfo, newClient);
     }
 
     @PUT
     @Consumes("application/json")
     @Produces("application/json")
-    public Response update(ResourceServerRepresentation server) {
+    public Response update(@Context UriInfo uriInfo, ResourceServerRepresentation server) {
         this.auth.requireManage();
         this.resourceServer.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement());
         this.resourceServer.setPolicyEnforcementMode(server.getPolicyEnforcementMode());
-
+        audit(OperationType.UPDATE, uriInfo, false);
         return Response.noContent().build();
     }
 
@@ -105,17 +115,19 @@ public class ResourceServerService {
         ResourceStore resourceStore = storeFactory.getResourceStore();
         String id = resourceServer.getId();
 
+        PolicyStore policyStore = storeFactory.getPolicyStore();
+
+        policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId()));
+
         resourceStore.findByResourceServer(id).forEach(resource -> resourceStore.delete(resource.getId()));
 
         ScopeStore scopeStore = storeFactory.getScopeStore();
 
         scopeStore.findByResourceServer(id).forEach(scope -> scopeStore.delete(scope.getId()));
 
-        PolicyStore policyStore = storeFactory.getPolicyStore();
-
-        policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId()));
-
         storeFactory.getResourceServerStore().delete(id);
+
+        audit(OperationType.DELETE, uriInfo, false);
     }
 
     @GET
@@ -143,12 +155,14 @@ public class ResourceServerService {
 
         RepresentationToModel.toModel(rep, authorization);
 
+        audit(OperationType.UPDATE, uriInfo, false);
+
         return Response.noContent().build();
     }
 
     @Path("/resource")
     public ResourceSetService getResourceSetResource() {
-        ResourceSetService resource = new ResourceSetService(this.resourceServer, this.authorization, this.auth);
+        ResourceSetService resource = new ResourceSetService(this.resourceServer, this.authorization, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
@@ -157,7 +171,7 @@ public class ResourceServerService {
 
     @Path("/scope")
     public ScopeService getScopeResource() {
-        ScopeService resource = new ScopeService(this.resourceServer, this.authorization, this.auth);
+        ScopeService resource = new ScopeService(this.resourceServer, this.authorization, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
@@ -166,7 +180,7 @@ public class ResourceServerService {
 
     @Path("/policy")
     public PolicyService getPolicyResource() {
-        PolicyService resource = new PolicyService(this.resourceServer, this.authorization, this.auth);
+        PolicyService resource = new PolicyService(this.resourceServer, this.authorization, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
@@ -176,7 +190,7 @@ public class ResourceServerService {
     @Path("/permission")
     public Object getPermissionTypeResource() {
         this.auth.requireView();
-        PermissionService resource = new PermissionService(this.resourceServer, this.authorization, this.auth);
+        PermissionService resource = new PermissionService(this.resourceServer, this.authorization, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
@@ -239,4 +253,14 @@ public class ResourceServerService {
             serviceAccount.grantRole(umaProtectionRole);
         }
     }
+
+    private void audit(OperationType operation, UriInfo uriInfo, boolean newClient) {
+        if (newClient) {
+            adminEvent.resource(ResourceType.AUTHORIZATION_RESOURCE_SERVER).operation(operation).resourcePath(uriInfo, client.getId())
+                    .representation(ModelToRepresentation.toRepresentation(resourceServer, client)).success();
+        } else {
+            adminEvent.resource(ResourceType.AUTHORIZATION_RESOURCE_SERVER).operation(operation).resourcePath(uriInfo)
+                    .representation(ModelToRepresentation.toRepresentation(resourceServer, client)).success();
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 5d65923..3c7f34f 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -26,6 +26,8 @@ import org.keycloak.authorization.model.Scope;
 import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.ResourceStore;
 import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
@@ -37,6 +39,7 @@ import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentatio
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
 import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 
 import javax.ws.rs.Consumes;
@@ -48,8 +51,10 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -70,17 +75,26 @@ public class ResourceSetService {
 
     private final AuthorizationProvider authorization;
     private final RealmAuth auth;
+    private final AdminEventBuilder adminEvent;
     private ResourceServer resourceServer;
 
-    public ResourceSetService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+    public ResourceSetService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.resourceServer = resourceServer;
         this.authorization = authorization;
         this.auth = auth;
+        this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_RESOURCE);
     }
 
     @POST
+    @NoCache
     @Consumes("application/json")
     @Produces("application/json")
+    public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
+        Response response = create(resource);
+        audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
+        return response;
+    }
+
     public Response create(ResourceRepresentation resource) {
         requireManage();
         StoreFactory storeFactory = this.authorization.getStoreFactory();
@@ -128,7 +142,7 @@ public class ResourceSetService {
     @PUT
     @Consumes("application/json")
     @Produces("application/json")
-    public Response update(@PathParam("id") String id, ResourceRepresentation resource) {
+    public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, ResourceRepresentation resource) {
         requireManage();
         resource.setId(id);
         StoreFactory storeFactory = this.authorization.getStoreFactory();
@@ -141,12 +155,14 @@ public class ResourceSetService {
 
         toModel(resource, resourceServer, authorization);
 
+        audit(uriInfo, resource, OperationType.UPDATE);
+
         return Response.noContent().build();
     }
 
     @Path("{id}")
     @DELETE
-    public Response delete(@PathParam("id") String id) {
+    public Response delete(@Context UriInfo uriInfo, @PathParam("id") String id) {
         requireManage();
         StoreFactory storeFactory = authorization.getStoreFactory();
         Resource resource = storeFactory.getResourceStore().findById(id, resourceServer.getId());
@@ -168,6 +184,10 @@ public class ResourceSetService {
 
         storeFactory.getResourceStore().delete(id);
 
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            audit(uriInfo, toRepresentation(resource, resourceServer, authorization), OperationType.DELETE);
+        }
+
         return Response.noContent().build();
     }
 
@@ -269,8 +289,8 @@ public class ResourceSetService {
 
     @Path("/search")
     @GET
-    @Produces("application/json")
     @NoCache
+    @Produces("application/json")
     public Response find(@QueryParam("name") String name) {
         this.auth.requireView();
         StoreFactory storeFactory = authorization.getStoreFactory();
@@ -376,4 +396,18 @@ public class ResourceSetService {
             this.auth.requireManage();
         }
     }
+
+    private void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, OperationType operation) {
+        audit(uriInfo, resource, null, operation);
+    }
+
+    private void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, String id, OperationType operation) {
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            if (id != null) {
+                adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();
+            } else {
+                adminEvent.operation(operation).resourcePath(uriInfo).representation(resource).success();
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index 1ec9a13..a2a2320 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -25,11 +25,14 @@ import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.model.Scope;
 import org.keycloak.authorization.store.PolicyStore;
 import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
 import org.keycloak.models.Constants;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
 import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.RealmAuth;
 
 import javax.ws.rs.Consumes;
@@ -41,9 +44,11 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -61,23 +66,28 @@ public class ScopeService {
 
     private final AuthorizationProvider authorization;
     private final RealmAuth auth;
+    private final AdminEventBuilder adminEvent;
     private ResourceServer resourceServer;
 
-    public ScopeService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+    public ScopeService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.resourceServer = resourceServer;
         this.authorization = authorization;
         this.auth = auth;
+        this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_SCOPE);
     }
 
     @POST
+    @NoCache
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
-    public Response create(ScopeRepresentation scope) {
+    public Response create(@Context UriInfo uriInfo,  ScopeRepresentation scope) {
         this.auth.requireManage();
         Scope model = toModel(scope, this.resourceServer, authorization);
 
         scope.setId(model.getId());
 
+        audit(uriInfo, scope, scope.getId(), OperationType.CREATE);
+
         return Response.status(Status.CREATED).entity(scope).build();
     }
 
@@ -85,7 +95,7 @@ public class ScopeService {
     @PUT
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
-    public Response update(@PathParam("id") String id, ScopeRepresentation scope) {
+    public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, ScopeRepresentation scope) {
         this.auth.requireManage();
         scope.setId(id);
         StoreFactory storeFactory = authorization.getStoreFactory();
@@ -97,12 +107,14 @@ public class ScopeService {
 
         toModel(scope, resourceServer, authorization);
 
+        audit(uriInfo, scope, OperationType.UPDATE);
+
         return Response.noContent().build();
     }
 
     @Path("{id}")
     @DELETE
-    public Response delete(@PathParam("id") String id) {
+    public Response delete(@Context UriInfo uriInfo, @PathParam("id") String id) {
         this.auth.requireManage();
         StoreFactory storeFactory = authorization.getStoreFactory();
         List<Resource> resources = storeFactory.getResourceStore().findByScope(Arrays.asList(id), resourceServer.getId());
@@ -130,11 +142,16 @@ public class ScopeService {
 
         storeFactory.getScopeStore().delete(id);
 
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            audit(uriInfo, toRepresentation(scope), OperationType.DELETE);
+        }
+
         return Response.noContent().build();
     }
 
     @Path("{id}")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response findById(@PathParam("id") String id) {
         this.auth.requireView();
@@ -144,11 +161,12 @@ public class ScopeService {
             return Response.status(Status.NOT_FOUND).build();
         }
 
-        return Response.ok(toRepresentation(model, this.authorization)).build();
+        return Response.ok(toRepresentation(model)).build();
     }
 
     @Path("{id}/resources")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response getResources(@PathParam("id") String id) {
         this.auth.requireView();
@@ -171,6 +189,7 @@ public class ScopeService {
 
     @Path("{id}/permissions")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response getPermissions(@PathParam("id") String id) {
         this.auth.requireView();
@@ -212,22 +231,18 @@ public class ScopeService {
             return Response.status(Status.OK).build();
         }
 
-        return Response.ok(toRepresentation(model, authorization)).build();
+        return Response.ok(toRepresentation(model)).build();
     }
 
     @GET
+    @NoCache
     @Produces("application/json")
     public Response findAll(@QueryParam("scopeId") String id,
                             @QueryParam("name") String name,
-                            @QueryParam("deep") Boolean deep,
                             @QueryParam("first") Integer firstResult,
                             @QueryParam("max") Integer maxResult) {
         this.auth.requireView();
 
-        if (deep == null) {
-            deep = true;
-        }
-
         Map<String, String[]> search = new HashMap<>();
 
         if (id != null && !"".equals(id.trim())) {
@@ -238,11 +253,24 @@ public class ScopeService {
             search.put("name", new String[] {name});
         }
 
-        Boolean finalDeep = deep;
         return Response.ok(
                 this.authorization.getStoreFactory().getScopeStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
-                        .map(scope -> toRepresentation(scope, this.authorization, finalDeep))
+                        .map(scope -> toRepresentation(scope))
                         .collect(Collectors.toList()))
                 .build();
     }
+
+    private void audit(@Context UriInfo uriInfo, ScopeRepresentation resource, OperationType operation) {
+        audit(uriInfo, resource, null, operation);
+    }
+
+    private void audit(@Context UriInfo uriInfo, ScopeRepresentation resource, String id, OperationType operation) {
+        if (authorization.getRealm().isAdminEventsEnabled()) {
+            if (id != null) {
+                adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();
+            } else {
+                adminEvent.operation(operation).resourcePath(uriInfo).representation(resource).success();
+            }
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
index 45fb6fe..3779279 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
@@ -27,12 +27,17 @@ import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.protection.permission.PermissionService;
 import org.keycloak.authorization.protection.permission.PermissionsService;
 import org.keycloak.authorization.protection.resource.ResourceService;
+import org.keycloak.common.ClientConnection;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.resources.admin.AdminAuth;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
 
 import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response.Status;
 
 /**
@@ -41,6 +46,8 @@ import javax.ws.rs.core.Response.Status;
 public class ProtectionService {
 
     private final AuthorizationProvider authorization;
+    @Context
+    protected ClientConnection clientConnection;
 
     public ProtectionService(AuthorizationProvider authorization) {
         this.authorization = authorization;
@@ -50,7 +57,12 @@ public class ProtectionService {
     public Object resource() {
         KeycloakIdentity identity = createIdentity();
         ResourceServer resourceServer = getResourceServer(identity);
-        ResourceSetService resourceManager = new ResourceSetService(resourceServer, this.authorization, null);
+        RealmModel realm = authorization.getRealm();
+        ClientModel client = realm.getClientById(identity.getId());
+        KeycloakSession keycloakSession = authorization.getKeycloakSession();
+        UserModel serviceAccount = keycloakSession.users().getServiceAccount(client);
+        AdminEventBuilder adminEvent = new AdminEventBuilder(realm, new AdminAuth(realm, identity.getAccessToken(), serviceAccount, client), keycloakSession, clientConnection);
+        ResourceSetService resourceManager = new ResourceSetService(resourceServer, this.authorization, null, adminEvent.realm(realm).authClient(client).authUser(serviceAccount));
 
         ResteasyProviderFactory.getInstance().injectProperties(resourceManager);
 
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
index 1695ba5..c2e11dc 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -31,8 +31,10 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
 
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.admin.ResourceSetService;
@@ -68,10 +70,10 @@ public class ResourceService {
     @POST
     @Consumes("application/json")
     @Produces("application/json")
-    public Response create(UmaResourceRepresentation umaResource) {
+    public Response create(@Context  UriInfo uriInfo, UmaResourceRepresentation umaResource) {
         checkResourceServerSettings();
         ResourceRepresentation resource = toResourceRepresentation(umaResource);
-        Response response = this.resourceManager.create(resource);
+        Response response = this.resourceManager.create(uriInfo, resource);
 
         if (response.getEntity() instanceof ResourceRepresentation) {
             return Response.status(Status.CREATED).entity(toUmaRepresentation((ResourceRepresentation) response.getEntity())).build();
@@ -84,9 +86,9 @@ public class ResourceService {
     @PUT
     @Consumes("application/json")
     @Produces("application/json")
-    public Response update(@PathParam("id") String id, UmaResourceRepresentation representation) {
+    public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, UmaResourceRepresentation representation) {
         ResourceRepresentation resource = toResourceRepresentation(representation);
-        Response response = this.resourceManager.update(id, resource);
+        Response response = this.resourceManager.update(uriInfo, id, resource);
 
         if (response.getEntity() instanceof ResourceRepresentation) {
             return Response.noContent().build();
@@ -97,9 +99,9 @@ public class ResourceService {
 
     @Path("/{id}")
     @DELETE
-    public Response delete(@PathParam("id") String id) {
+    public Response delete(@Context UriInfo uriInfo, @PathParam("id") String id) {
         checkResourceServerSettings();
-        return this.resourceManager.delete(id);
+        return this.resourceManager.delete(uriInfo, id);
     }
 
     @Path("/{id}")
diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
new file mode 100644
index 0000000..4856fb6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) eHealth
+ */
+package org.keycloak.broker.saml.mappers;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:frelibert@yahoo.com">Frederik Libert</a>
+ *
+ */
+public class UserAttributeStatementMapper extends AbstractIdentityProviderMapper {
+
+    private static final String USER_ATTR_LOCALE = "locale";
+
+    private static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
+
+    public static final String ATTRIBUTE_NAME_PATTERN = "attribute.name.pattern";
+
+    public static final String USER_ATTRIBUTE_FIRST_NAME = "user.attribute.firstName";
+
+    public static final String USER_ATTRIBUTE_LAST_NAME = "user.attribute.lastName";
+
+    public static final String USER_ATTRIBUTE_EMAIL = "user.attribute.email";
+
+    public static final String USER_ATTRIBUTE_LANGUAGE = "user.attribute.language";
+    
+    private static final String USE_FRIENDLY_NAMES = "use.friendly.names";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(ATTRIBUTE_NAME_PATTERN);
+        property.setLabel("Attribute Name Pattern");
+        property.setHelpText("Pattern of attribute names in assertion that must be mapped. Leave blank to map all attributes.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_FIRST_NAME);
+        property.setLabel("User Attribute FirstName");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property firstName.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_LAST_NAME);
+        property.setLabel("User Attribute LastName");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property lastName.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_EMAIL);
+        property.setLabel("User Attribute Email");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property email.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_LANGUAGE);
+        property.setLabel("User Attribute Language");
+        property.setHelpText("Define which saml Attribute must be mapped to the User attribute locale.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USE_FRIENDLY_NAMES);
+        property.setLabel("Use Attribute Friendly Name");
+        property.setHelpText("Define which name to give to each mapped user attribute: name or friendlyName.");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        CONFIG_PROPERTIES.add(property);
+    }
+
+    public static final String PROVIDER_ID = "saml-user-attributestatement-idp-mapper";
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String[] getCompatibleProviders() {
+        return COMPATIBLE_PROVIDERS.clone();
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return "AttributeStatement Importer";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "AttributeStatement Importer";
+    }
+
+    @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+        String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+        String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+        String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+        Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+        List<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+        for (AttributeType a : attributesInContext) {
+            String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+            List<String> attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+            if (!attributeValuesInContext.isEmpty()) {
+                // set as attribute anyway
+                context.setUserAttribute(attribute, attributeValuesInContext);
+                // set as special field ?
+                if (Objects.equals(attribute, emailAttribute)) {
+                    setIfNotEmpty(context::setEmail, attributeValuesInContext);
+                } else if (Objects.equals(attribute, firstNameAttribute)) {
+                    setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+                } else if (Objects.equals(attribute, lastNameAttribute)) {
+                    setIfNotEmpty(context::setLastName, attributeValuesInContext);
+                } else if (Objects.equals(attribute, langAttribute)) {
+                    context.setUserAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+                } 
+            }
+        }
+    }
+
+    @Override
+    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+        String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+        String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+        String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+        Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+        List<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+
+        Set<String> assertedUserAttributes = new HashSet<String>();
+        for (AttributeType a : attributesInContext) {
+            String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+            List<String> attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+            List<String> currentAttributeValues = user.getAttributes().get(attribute);
+            if (attributeValuesInContext == null) {
+                // attribute no longer sent by brokered idp, remove it
+                user.removeAttribute(attribute);
+            } else if (currentAttributeValues == null) {
+                // new attribute sent by brokered idp, add it
+                user.setAttribute(attribute, attributeValuesInContext);
+            } else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
+                // attribute sent by brokered idp has different values as before, update it
+                user.setAttribute(attribute, attributeValuesInContext);
+            }
+            if (Objects.equals(attribute, emailAttribute)) {
+                setIfNotEmpty(context::setEmail, attributeValuesInContext);
+            } else if (Objects.equals(attribute, firstNameAttribute)) {
+                setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+            } else if (Objects.equals(attribute, lastNameAttribute)) {
+                setIfNotEmpty(context::setLastName, attributeValuesInContext);
+            } else if (Objects.equals(attribute, langAttribute)) {
+                if(attributeValuesInContext == null) {
+                    user.removeAttribute(USER_ATTR_LOCALE);
+                } else {
+                    user.setAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+                }
+                assertedUserAttributes.add(USER_ATTR_LOCALE);
+            } 
+            // Mark attribute as handled
+            assertedUserAttributes.add(attribute);
+        }
+        // Remove user attributes that were not referenced in assertion.
+        user.getAttributes().keySet().stream().filter(a -> !assertedUserAttributes.contains(a)).forEach(a -> user.removeAttribute(a));
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Import all saml attributes found in attributestatements in assertion into user properties or attributes.";
+    }
+
+    private Optional<Pattern> getAttributePattern(IdentityProviderMapperModel mapperModel) {
+        String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN);
+        return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null);
+    }
+
+    private List<AttributeType> findAttributesInContext(BrokeredIdentityContext context, Optional<Pattern> attributePattern) {
+        AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+
+        return assertion.getAttributeStatements().stream()//
+            .flatMap(statement -> statement.getAttributes().stream())//
+            .filter(item -> !attributePattern.isPresent() || attributePattern.get().matcher(item.getAttribute().getName()).matches())//
+            .map(ASTChoiceType::getAttribute)//
+            .collect(Collectors.toList());
+    }
+
+    private void setIfNotEmpty(Consumer<String> consumer, List<String> values) {
+        if (values != null && !values.isEmpty()) {
+            consumer.accept(values.get(0));
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index dc38d59..4451b8c 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -372,13 +372,13 @@ public class SAMLEndpoint {
                     assertionElement = DocumentUtil.getElement(holder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get()));
                 }
 
-                if (config.isWantAssertionsSigned() && config.isValidateSignature()) {
-                    if (!AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator())) {
-                        logger.error("validation failed");
-                        event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
-                        event.error(Errors.INVALID_SIGNATURE);
-                        return ErrorPage.error(session, Messages.INVALID_REQUESTER);
-                    }
+                boolean signed = AssertionUtil.isSignedElement(assertionElement);
+                if ((config.isWantAssertionsSigned() && !signed)
+                        || (signed && config.isValidateSignature() && !AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator()))) {
+                    logger.error("validation failed");
+                    event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+                    event.error(Errors.INVALID_SIGNATURE);
+                    return ErrorPage.error(session, Messages.INVALID_REQUESTER);
                 }
 
                 AssertionType assertion = responseType.getAssertions().get(0).getAssertion();
diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 8f2e153..facce6c 100755
--- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -343,7 +343,7 @@ public class ExportUtils {
         representation.setPolicies(policies);
 
         List<ScopeRepresentation> scopes = storeFactory.getScopeStore().findByResourceServer(settingsModel.getId()).stream().map(scope -> {
-            ScopeRepresentation rep = toRepresentation(scope, authorization);
+            ScopeRepresentation rep = toRepresentation(scope);
 
             rep.setId(null);
             rep.setPolicies(null);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index affaf20..d7eb01c 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -76,6 +76,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
     private String accessRequestMessage;
     private URI actionUri;
+    private String execution;
 
     private List<FormMessage> messages = null;
     private MessageType messageType = MessageType.ERROR;
@@ -230,6 +231,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
                         b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
                         break;
                 }
+
+                if (execution != null) {
+                    b.queryParam(Constants.EXECUTION, execution);
+                }
+
                 attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
             }
         }
@@ -366,7 +372,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
             attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
 
             if (realm.isInternationalizationEnabled()) {
-                UriBuilder b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
+                UriBuilder b = UriBuilder.fromUri(baseUri)
+                        .path(uriInfo.getPath());
+
+                if (execution != null) {
+                    b.queryParam(Constants.EXECUTION, execution);
+                }
+
                 attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
             }
         }
@@ -591,6 +603,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     }
 
     @Override
+    public LoginFormsProvider setExecution(String execution) {
+        this.execution = execution;
+        return this;
+    }
+
+    @Override
     public LoginFormsProvider setResponseHeader(String headerName, String headerValue) {
         this.httpResponseHeaders.put(headerName, headerValue);
         return this;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 26d012b..3a7e4c0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -269,6 +269,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
     }
 
     private Response checkOIDCParams() {
+        // If request is not OIDC request, but pure OAuth2 request and response_type is just 'token', then 'nonce' is not mandatory
+        boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
+        if (!isOIDCRequest && parsedResponseType.toString().equals(OIDCResponseType.TOKEN)) {
+            return null;
+        }
+
         if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
             ServicesLogger.LOGGER.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
             event.error(Errors.INVALID_REQUEST);
@@ -354,10 +360,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
 
     private void checkRedirectUri() {
         String redirectUriParam = request.getRedirectUriParam();
+        boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
 
         event.detail(Details.REDIRECT_URI, redirectUriParam);
 
-        redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
+        // redirect_uri parameter is required per OpenID Connect, but optional per OAuth2
+        redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client, isOIDCRequest);
         if (redirectUri == null) {
             event.error(Errors.INVALID_REDIRECT_URI);
             throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 6aa13e2..4870415 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -560,13 +560,9 @@ public class TokenEndpoint {
     // https://tools.ietf.org/html/rfc7636#section-4.6
     private String generateS256CodeChallenge(String codeVerifier) throws Exception {
         MessageDigest md = MessageDigest.getInstance("SHA-256");
-        md.update(codeVerifier.getBytes());
-        StringBuilder sb = new StringBuilder();
-        for (byte b : md.digest()) {
-            String hex = String.format("%02x", b);
-            sb.append(hex);
-        }
-        String codeVerifierEncoded = Base64Url.encode(sb.toString().getBytes());
+        md.update(codeVerifier.getBytes("ISO_8859_1"));
+        byte[] digestBytes = md.digest();
+        String codeVerifierEncoded = Base64Url.encode(digestBytes);
         return codeVerifierEncoded;
     }
  
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
index dfae565..4fc73f7 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
@@ -151,7 +151,7 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
     }
 
     private void configureAuthorizationSettings(KeycloakSession session, ClientModel client, ClientManager.InstallationAdapterConfig rep) {
-        if (new AuthorizationService(session, client, null).isEnabled()) {
+        if (new AuthorizationService(session, client, null, null).isEnabled()) {
             PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig();
 
             enforcerConfig.setEnforcementMode(null);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
index 09b39c4..374581d 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
@@ -64,24 +64,32 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
     }
 
     @Override
-    public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
-        setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
+    public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+        setIDTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
         return token;
     }
 
     @Override
-    public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
-        setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
+    public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+        setAccessTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
         return token;
     }
 
     @Override
-    public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
-        setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
+    public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+        setUserInfoTokenSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
         return token;
     }
 
-    private void setSubject(IDToken token, String pairwiseSub) {
+    protected void setIDTokenSubject(IDToken token, String pairwiseSub) {
+        token.setSubject(pairwiseSub);
+    }
+
+    protected void setAccessTokenSubject(IDToken token, String pairwiseSub) {
+        token.setSubject(pairwiseSub);
+    }
+
+    protected void setUserInfoTokenSubject(IDToken token, String pairwiseSub) {
         token.getOtherClaims().put("sub", pairwiseSub);
     }
 
@@ -115,6 +123,4 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
     public final String getId() {
         return "oidc-" + getIdPrefix() + PROVIDER_ID_SUFFIX;
     }
-}
-
-
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA256PairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA256PairwiseSubMapper.java
index 28a0b87..f69f2b1 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA256PairwiseSubMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA256PairwiseSubMapper.java
@@ -79,7 +79,7 @@ public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
         Charset charset = Charset.forName("UTF-8");
         byte[] salt = saltStr.getBytes(charset);
         String pairwiseSub = generateSub(sectorIdentifier, localSub, salt);
-        logger.infof("local sub = '%s', pairwise sub = '%s'", localSub, pairwiseSub);
+        logger.tracef("local sub = '%s', pairwise sub = '%s'", localSub, pairwiseSub);
         return pairwiseSub;
     }
 
@@ -109,4 +109,4 @@ public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
     public String getIdPrefix() {
         return PROVIDER_ID;
     }
-}
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
index 60f5493..c61bdd0 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
@@ -26,6 +26,7 @@ import org.keycloak.services.Urls;
 
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -38,12 +39,16 @@ public class RedirectUtils {
 
     public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) {
         Set<String> validRedirects = getValidateRedirectUris(uriInfo, realm);
-        return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects);
+        return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects, true);
     }
 
     public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
+        return verifyRedirectUri(uriInfo, redirectUri, realm, client, true);
+    }
+
+    public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client, boolean requireRedirectUri) {
         if (client != null)
-            return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris());
+            return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris(), requireRedirectUri);
         return null;
     }
 
@@ -69,10 +74,16 @@ public class RedirectUtils {
         return redirects;
     }
 
-    private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> validRedirects) {
+    private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> validRedirects, boolean requireRedirectUri) {
         if (redirectUri == null) {
-            logger.debug("No Redirect URI parameter specified");
-            return null;
+            if (!requireRedirectUri) {
+                redirectUri = getSingleValidRedirectUri(validRedirects);
+            }
+
+            if (redirectUri == null) {
+                logger.debug("No Redirect URI parameter specified");
+                return null;
+            }
         } else if (validRedirects.isEmpty()) {
             logger.debug("No Redirect URIs supplied");
             redirectUri = null;
@@ -149,4 +160,14 @@ public class RedirectUtils {
         return false;
     }
 
+    private static String getSingleValidRedirectUri(Collection<String> validRedirects) {
+        if (validRedirects.size() != 1) return null;
+        String validRedirect = validRedirects.iterator().next();
+        int idx = validRedirect.indexOf("/*");
+        if (idx > -1) {
+            validRedirect = validRedirect.substring(0, idx);
+        }
+        return validRedirect;
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..534883a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
@@ -0,0 +1,76 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Abstract class for wrapping a {@link ScriptModel} to make it evaluatable.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+abstract class AbstractEvaluatableScriptAdapter implements EvaluatableScriptAdapter {
+    /**
+     * Holds the {@link ScriptModel}.
+     */
+    private final ScriptModel scriptModel;
+
+    AbstractEvaluatableScriptAdapter(final ScriptModel scriptModel) {
+        if (scriptModel == null) {
+            throw new IllegalArgumentException("scriptModel must not be null");
+        }
+        this.scriptModel = scriptModel;
+    }
+
+    @Override
+    public Object eval(final ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException {
+        return evalUnchecked(createBindings(bindingsConfigurer));
+    }
+
+    @Override
+    public ScriptModel getScriptModel() {
+        return scriptModel;
+    }
+
+    /**
+     * Note, calling this method modifies the underlying {@link ScriptEngine},
+     * preventing concurrent use of the ScriptEngine (Nashorn's {@link ScriptEngine} and
+     * {@link javax.script.CompiledScript} is thread-safe, but {@link Bindings} isn't).
+     */
+    InvocableScriptAdapter prepareInvokableScript(final ScriptBindingsConfigurer bindingsConfigurer) {
+        final Bindings bindings = createBindings(bindingsConfigurer);
+        evalUnchecked(bindings);
+        final ScriptEngine engine = getEngine();
+        engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+        return new InvocableScriptAdapter(scriptModel, engine);
+    }
+
+    protected String getCode() {
+        return scriptModel.getCode();
+    }
+
+    protected abstract ScriptEngine getEngine();
+
+    protected abstract Object eval(Bindings bindings) throws ScriptException;
+
+    private Object evalUnchecked(final Bindings bindings) {
+        try {
+            return eval(bindings);
+        }
+        catch (ScriptException e) {
+            throw new ScriptExecutionException(scriptModel, e);
+        }
+    }
+
+    private Bindings createBindings(final ScriptBindingsConfigurer bindingsConfigurer) {
+        if (bindingsConfigurer == null) {
+            throw new IllegalArgumentException("bindingsConfigurer must not be null");
+        }
+        final Bindings bindings = getEngine().createBindings();
+        bindingsConfigurer.configureBindings(bindings);
+        return bindings;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..7359dc9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,40 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.CompiledScript;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a compiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+class CompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+    /**
+     * Holds the {@link CompiledScript} for the {@link ScriptModel}.
+     */
+    private final CompiledScript compiledScript;
+
+    CompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final CompiledScript compiledScript) {
+        super(scriptModel);
+
+        if (compiledScript == null) {
+            throw new IllegalArgumentException("compiledScript must not be null");
+        }
+
+        this.compiledScript = compiledScript;
+    }
+
+    @Override
+    protected ScriptEngine getEngine() {
+        return compiledScript.getEngine();
+    }
+
+    @Override
+    protected Object eval(final Bindings bindings) throws ScriptException {
+        return compiledScript.eval(bindings);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
index 601da8e..d781460 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
@@ -16,12 +16,14 @@
  */
 package org.keycloak.scripting;
 
-import org.keycloak.models.ScriptModel;
-
 import javax.script.Bindings;
-import javax.script.ScriptContext;
+import javax.script.Compilable;
+import javax.script.CompiledScript;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
 
 /**
  * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@@ -32,8 +34,7 @@ public class DefaultScriptingProvider implements ScriptingProvider {
 
     private final ScriptEngineManager scriptEngineManager;
 
-    public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
-
+    DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
         if (scriptEngineManager == null) {
             throw new IllegalStateException("scriptEngineManager must not be null!");
         }
@@ -44,13 +45,22 @@ public class DefaultScriptingProvider implements ScriptingProvider {
     /**
      * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
      *
-     * @param scriptModel  must not be {@literal null}
+     * @param scriptModel        must not be {@literal null}
      * @param bindingsConfigurer must not be {@literal null}
-     * @return
      */
     @Override
     public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
+        final AbstractEvaluatableScriptAdapter evaluatable = prepareEvaluatableScript(scriptModel);
+        return evaluatable.prepareInvokableScript(bindingsConfigurer);
+    }
 
+    /**
+     * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
+     *
+     * @param scriptModel must not be {@literal null}
+     */
+    @Override
+    public AbstractEvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel) {
         if (scriptModel == null) {
             throw new IllegalArgumentException("script must not be null");
         }
@@ -59,13 +69,18 @@ public class DefaultScriptingProvider implements ScriptingProvider {
             throw new IllegalArgumentException("script must not be null or empty");
         }
 
-        if (bindingsConfigurer == null) {
-            throw new IllegalArgumentException("bindingsConfigurer must not be null");
-        }
+        ScriptEngine engine = createPreparedScriptEngine(scriptModel);
 
-        ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
-
-        return new InvocableScriptAdapter(scriptModel, engine);
+        if (engine instanceof Compilable) {
+            try {
+                final CompiledScript compiledScript = ((Compilable) engine).compile(scriptModel.getCode());
+                return new CompiledEvaluatableScriptAdapter(scriptModel, compiledScript);
+            }
+            catch (ScriptException e) {
+                throw new ScriptExecutionException(scriptModel, e);
+            }
+        }
+        return new UncompiledEvaluatableScriptAdapter(scriptModel, engine);
     }
 
     //TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
@@ -74,38 +89,27 @@ public class DefaultScriptingProvider implements ScriptingProvider {
 
     @Override
     public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
+        return new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
+    }
 
-        ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
-        return script;
+    @Override
+    public void close() {
+        //NOOP
     }
 
     /**
      * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
-     *
-     * @param script
-     * @param bindingsConfigurer
-     * @return
      */
-    private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
-
+    private ScriptEngine createPreparedScriptEngine(ScriptModel script) {
         ScriptEngine scriptEngine = lookupScriptEngineFor(script);
 
         if (scriptEngine == null) {
             throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
         }
 
-        configureBindings(bindingsConfigurer, scriptEngine);
-
         return scriptEngine;
     }
 
-    private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
-
-        Bindings bindings = engine.createBindings();
-        bindingsConfigurer.configureBindings(bindings);
-        engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
-    }
-
     /**
      * Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
      */
@@ -114,13 +118,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
         try {
             Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader());
             return scriptEngineManager.getEngineByMimeType(script.getMimeType());
-        } finally {
+        }
+        finally {
             Thread.currentThread().setContextClassLoader(cl);
         }
     }
-
-    @Override
-    public void close() {
-        //NOOP
-    }
 }
diff --git a/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..8464fdf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,39 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps an uncompiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+class UncompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+    /**
+     * Holds the {@link ScriptEngine} instance.
+     */
+    private final ScriptEngine scriptEngine;
+
+    UncompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final ScriptEngine scriptEngine) {
+        super(scriptModel);
+        if (scriptEngine == null) {
+            throw new IllegalArgumentException("scriptEngine must not be null");
+        }
+
+        this.scriptEngine = scriptEngine;
+    }
+
+    @Override
+    protected ScriptEngine getEngine() {
+        return scriptEngine;
+    }
+
+    @Override
+    protected Object eval(final Bindings bindings) throws ScriptException {
+        return getEngine().eval(getCode(), bindings);
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
index 2974b6c..da693aa 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -23,6 +23,7 @@ import org.keycloak.models.ClientInitialAccessModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.representations.idm.ClientRepresentation;
@@ -65,7 +66,8 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
         }
 
         try {
-            ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
+            RealmModel realm = session.getContext().getRealm();
+            ClientModel clientModel = RepresentationToModel.createClient(session, realm, client, true);
 
             ClientRegistrationPolicyManager.triggerAfterRegister(context, registrationAuth, clientModel);
 
@@ -78,7 +80,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
 
             if (auth.isInitialAccessToken()) {
                 ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
-                initialAccessModel.decreaseRemainingCount();
+                session.realms().decreaseRemainingCount(realm, initialAccessModel);
             }
 
             event.client(client.getClientId()).success();
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
index 8382155..9b2b284 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java
@@ -85,7 +85,7 @@ public class ClientRegistrationAuth {
         jwt = tokenVerification.getJwt();
 
         if (isInitialAccessToken()) {
-            initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
+            initialAccessModel = session.realms().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
             if (initialAccessModel == null) {
                 throw unauthorized("Initial Access Token not found");
             }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 07bd1f6..6c91759 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -644,12 +644,15 @@ public class AuthenticationManager {
 
             // Skip grant screen if everything was already approved by this user
             if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
+                String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name();
+
                 accessCode.
 
                         setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
-                authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
+                authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);
 
                 return session.getProvider(LoginFormsProvider.class)
+                        .setExecution(execution)
                         .setClientSessionCode(accessCode.getCode())
                         .setAccessRequest(realmRoles, resourceRoles, protocolMappers)
                         .createOAuthGrant();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
index dfd28cc..0fe0090 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
@@ -81,7 +81,7 @@ public class ClientInitialAccessResource {
         int expiration = config.getExpiration() != null ? config.getExpiration() : 0;
         int count = config.getCount() != null ? config.getCount() : 1;
 
-        ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count);
+        ClientInitialAccessModel clientInitialAccessModel = session.realms().createClientInitialAccessModel(realm, expiration, count);
 
         adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success();
 
@@ -101,7 +101,7 @@ public class ClientInitialAccessResource {
     public List<ClientInitialAccessPresentation> list() {
         auth.requireView();
 
-        List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
+        List<ClientInitialAccessModel> models = session.realms().listClientInitialAccess(realm);
         List<ClientInitialAccessPresentation> reps = new LinkedList<>();
         for (ClientInitialAccessModel m : models) {
             ClientInitialAccessPresentation r = wrap(m);
@@ -115,7 +115,7 @@ public class ClientInitialAccessResource {
     public void delete(final @PathParam("id") String id) {
         auth.requireManage();
 
-        session.sessions().removeClientInitialAccessModel(realm, id);
+        session.realms().removeClientInitialAccessModel(realm, id);
         adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index a92729e..43c897c 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -148,36 +148,13 @@ public class ClientResource {
         try {
             updateClientFromRep(rep, client, session);
             adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
+            updateAuthorizationSettings(rep);
             return Response.noContent().build();
         } catch (ModelDuplicateException e) {
             return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
         }
     }
 
-    public void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
-        if (TRUE.equals(rep.isServiceAccountsEnabled())) {
-            UserModel serviceAccount = this.session.users().getServiceAccount(client);
-
-            if (serviceAccount == null) {
-                new ClientManager(new RealmManager(session)).enableServiceAccount(client);
-            }
-        }
-
-        if (!rep.getClientId().equals(client.getClientId())) {
-            new ClientManager(new RealmManager(session)).clientIdChanged(client, rep.getClientId());
-        }
-
-        RepresentationToModel.updateClient(rep, client);
-
-        if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
-            if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
-                authorization().enable();
-            } else {
-                authorization().disable();
-            }
-        }
-    }
-
     /**
      * Get representation of the client
      *
@@ -587,10 +564,36 @@ public class ClientResource {
     public AuthorizationService authorization() {
         ProfileHelper.requireFeature(Profile.Feature.AUTHORIZATION);
 
-        AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth);
+        AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth, adminEvent);
 
         ResteasyProviderFactory.getInstance().injectProperties(resource);
 
         return resource;
     }
+
+    private void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
+        if (TRUE.equals(rep.isServiceAccountsEnabled())) {
+            UserModel serviceAccount = this.session.users().getServiceAccount(client);
+
+            if (serviceAccount == null) {
+                new ClientManager(new RealmManager(session)).enableServiceAccount(client);
+            }
+        }
+
+        if (!rep.getClientId().equals(client.getClientId())) {
+            new ClientManager(new RealmManager(session)).clientIdChanged(client, rep.getClientId());
+        }
+
+        RepresentationToModel.updateClient(rep, client);
+    }
+
+    private void updateAuthorizationSettings(ClientRepresentation rep) {
+        if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
+            if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
+                authorization().enable(false);
+            } else {
+                authorization().disable();
+            }
+        }
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index 3fa3c75..a488ba3 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -129,7 +129,7 @@ public class ClientsResource {
     }
 
     private AuthorizationService getAuthorizationService(ClientModel clientModel) {
-        return new AuthorizationService(session, clientModel, auth);
+        return new AuthorizationService(session, clientModel, auth, adminEvent);
     }
 
     /**
@@ -167,14 +167,14 @@ public class ClientsResource {
                 }
             }
 
+            adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientModel.getId()).representation(rep).success();
+
             if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
                 if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
-                    getAuthorizationService(clientModel).enable();
+                    getAuthorizationService(clientModel).enable(true);
                 }
             }
 
-            adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientModel.getId()).representation(rep).success();
-
             return Response.created(uriInfo.getAbsolutePathBuilder().path(clientModel.getId()).build()).build();
         } catch (ModelDuplicateException e) {
             return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 15315d6..42a2fd8 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -47,6 +47,7 @@ import org.keycloak.services.managers.ApplianceBootstrap;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.UserStorageSyncManager;
 import org.keycloak.services.resources.admin.AdminRoot;
+import org.keycloak.services.scheduled.ClearExpiredClientInitialAccessTokens;
 import org.keycloak.services.scheduled.ClearExpiredEvents;
 import org.keycloak.services.scheduled.ClearExpiredUserSessions;
 import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
@@ -70,8 +71,10 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URL;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -88,6 +91,8 @@ public class KeycloakApplication extends Application {
 
     public static final String KEYCLOAK_EMBEDDED = "keycloak.embedded";
 
+    public static final String SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES = "keycloak.server.context.config.property-overrides";
+
     private static final Logger logger = Logger.getLogger(KeycloakApplication.class);
 
     protected boolean embedded = false;
@@ -262,7 +267,7 @@ public class KeycloakApplication extends Application {
     public static void loadConfig(ServletContext context) {
         try {
             JsonNode node = null;
-            
+
             String dmrConfig = loadDmrConfig(context);
             if (dmrConfig != null) {
                 node = new ObjectMapper().readTree(dmrConfig);
@@ -287,7 +292,13 @@ public class KeycloakApplication extends Application {
             }
 
             if (node != null) {
-                Properties properties = new SystemEnvProperties();
+                Map<String, String> propertyOverridesMap = new HashMap<>();
+                String propertyOverrides = context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES);
+                if (context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES) != null) {
+                    JsonNode jsonObj = new ObjectMapper().readTree(propertyOverrides);
+                    jsonObj.fields().forEachRemaining(e -> propertyOverridesMap.put(e.getKey(), e.getValue().asText()));
+                }
+                Properties properties = new SystemEnvProperties(propertyOverridesMap);
                 Config.init(new JsonConfigProvider(node, properties));
             } else {
                 throw new RuntimeException("Keycloak config not found.");
@@ -321,6 +332,7 @@ public class KeycloakApplication extends Application {
         try {
             TimerProvider timer = session.getProvider(TimerProvider.class);
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
+            timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens");
             timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
             new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
         } finally {
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java
new file mode 100644
index 0000000..94a4f6b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 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.services.scheduled;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.timer.ScheduledTask;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClearExpiredClientInitialAccessTokens implements ScheduledTask {
+
+    @Override
+    public void run(KeycloakSession session) {
+        session.realms().removeExpiredClientInitialAccess();
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index bd239a6..2d6c473 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -406,7 +406,7 @@ public interface ServicesLogger extends BasicLogger {
     void failedToCloseProviderSession(@Cause Throwable t);
 
     @LogMessage(level = WARN)
-    @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request. This can have impact in future versions (eg. removed IDToken from the Token Response)")
+    @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request.")
     @Once
     void oidcScopeMissing();
 
diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
index 3726b99..489f73f 100644
--- a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
@@ -58,6 +58,7 @@ public class AuthenticationFlowURLHelper {
 
         return session.getProvider(LoginFormsProvider.class)
                 .setActionUri(lastStepUrl)
+                .setExecution(getExecutionId(authSession))
                 .createLoginExpiredPage();
     }
 
@@ -76,7 +77,7 @@ public class AuthenticationFlowURLHelper {
 
 
     public URI getLastExecutionUrl(AuthenticationSessionModel authSession) {
-        String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+        String executionId = getExecutionId(authSession);
         String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
 
         if (latestFlowPath == null) {
@@ -90,4 +91,8 @@ public class AuthenticationFlowURLHelper {
         return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId());
     }
 
+    private String getExecutionId(AuthenticationSessionModel authSession) {
+        return authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+    }
+
 }
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 10becee..cd818c2 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -45,7 +45,23 @@ and adapter are all in the same JVM and you can debug them easily. If it is not 
    
 and you will be able to attach remote debugger to the test. Unfortunately server and adapter are running in different JVMs, so this won't help to debug those. 
 
-TODO: Improve and add more info about Wildfly debugging...
+### JBoss auth server debugging
+
+When tests are run on JBoss based container (WildFly/EAP) there is possibility to attach a debugger, by default on localhost:5005.
+
+The server won't wait to attach the debugger. There are some properties what can change the default behaviour.
+
+    -Dauth.server.debug.port=$PORT
+    -Dauth.server.debug.suspend=y
+
+More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching.html
+
+### JBoss app server debugging
+
+Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties.
+
+    -Dapp.server.debug.port=$PORT
+    -Dapp.server.debug.suspend=y    
 
 ## Testsuite logging
 
@@ -417,4 +433,34 @@ and argument: `-p 8181`
 
 3) Run loadbalancer (class `SimpleUndertowLoadBalancer`) without arguments and system properties. Loadbalancer runs on port 8180, so you can access Keycloak on `http://localhost:8180/auth`     
 
+## Cross-DC tests
+
+Cross-DC tests use 2 data centers, each with one automatically started and one manually controlled backend servers
+(currently only Keycloak on Undertow), and 1 frontend loadbalancer server node that sits in front of all servers.
+The browser usually communicates directly with the frontent node and the test controls where the HTTP requests
+land by adjusting load balancer configuration (e.g. to direct the traffic to only a single DC).
+
+For an example of a test, see [org.keycloak.testsuite.crossdc.ActionTokenCrossDCTest](tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java).
+
+The cross DC requires setting a profile specifying used cache server (currently only Infinispan) by specifying
+`cache-server-infinispan` profile in maven.
+
+#### Run Cross-DC Tests from Maven
+
+First compile the Infinispan/JDG test server via the following command:
+
+  `mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install`
+
+or
+  
+  `mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install`
+
+Then you can run the tests using the following command (adjust the test specification according to your needs):
+
+  `mvn -Pcache-server-infinispan -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
+
+or
+
+  `mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
 
+_Someone using IntelliJ IDEA, please describe steps for that IDE_
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index 0219f40..7e36d1a 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -46,6 +46,7 @@
         <arquillian-drone.version>2.0.1.Final</arquillian-drone.version>
         <arquillian-graphene.version>2.1.0.Alpha3</arquillian-graphene.version>
         <arquillian-wildfly-container.version>2.1.0.Alpha2</arquillian-wildfly-container.version>
+        <arquillian-infinispan-container.version>1.2.0.Beta2</arquillian-infinispan-container.version>
         <version.shrinkwrap.resolvers>2.2.2</version.shrinkwrap.resolvers>
         <undertow-embedded.version>1.0.0.Alpha2</undertow-embedded.version>
 
@@ -88,6 +89,11 @@
                 <scope>import</scope>
             </dependency>
             <dependency>
+                <groupId>org.infinispan.arquillian.container</groupId>
+                <artifactId>infinispan-arquillian-impl</artifactId>
+                <version>${arquillian-infinispan-container.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.wildfly.arquillian</groupId>
                 <artifactId>wildfly-arquillian-container-managed</artifactId>
                 <version>${arquillian-wildfly-container.version}</version>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java
new file mode 100644
index 0000000..1e435b2
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/arquillian/LoadBalancerController.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 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.arquillian;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface LoadBalancerController {
+
+    void enableAllBackendNodes();
+
+    void disableAllBackendNodes();
+
+    void enableBackendNodeByName(String nodeName);
+
+    void disableBackendNodeByName(String nodeName);
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index b868827..f18bbbd 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -164,6 +164,8 @@ public class TestingResourceProvider implements RealmResourceProvider {
 
         session.sessions().removeExpired(realm);
         session.authenticationSessions().removeExpired(realm);
+        session.realms().removeExpiredClientInitialAccess();
+
         return Response.ok().build();
     }
 
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
index 4298d35..fdcb092 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml
@@ -71,6 +71,11 @@
             <artifactId>keycloak-undertow-adapter</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.keycloak.testsuite</groupId>
+            <artifactId>integration-arquillian-testsuite-providers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-servlet-filter-adapter</artifactId>
         </dependency>
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 87a37d2..b77d9e0 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
@@ -48,6 +48,8 @@ import org.keycloak.services.filters.KeycloakSessionServletFilter;
 import org.keycloak.services.managers.ApplianceBootstrap;
 import org.keycloak.services.resources.KeycloakApplication;
 
+import org.keycloak.util.JsonSerialization;
+import java.io.IOException;
 import javax.servlet.DispatcherType;
 import javax.servlet.ServletException;
 
@@ -55,6 +57,7 @@ import java.lang.reflect.Field;
 import java.util.Collection;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
 
 public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUndertowConfiguration> {
 
@@ -75,6 +78,14 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
         di.setContextPath("/auth");
         di.setDeploymentName("Keycloak");
         di.addInitParameter(KeycloakApplication.KEYCLOAK_EMBEDDED, "true");
+        if (configuration.getKeycloakConfigPropertyOverridesMap() != null) {
+            try {
+                di.addInitParameter(KeycloakApplication.SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES,
+                  JsonSerialization.writeValueAsString(configuration.getKeycloakConfigPropertyOverridesMap()));
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
 
         di.setDefaultServletConfig(new DefaultServletConfig(true));
         di.addWelcomePage("theme/keycloak/welcome/resources/index.html");
@@ -176,19 +187,14 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
             log.info("Using route: " + configuration.getRoute());
         }
 
-        SetSystemProperty setRouteProperty = new SetSystemProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, configuration.getRoute());
-        try {
-            DeploymentInfo di = createAuthServerDeploymentInfo();
-            undertow.deploy(di);
-            ResteasyDeployment deployment = (ResteasyDeployment) di.getServletContextAttributes().get(ResteasyDeployment.class.getName());
-            sessionFactory = ((KeycloakApplication) deployment.getApplication()).getSessionFactory();
+        DeploymentInfo di = createAuthServerDeploymentInfo();
+        undertow.deploy(di);
+        ResteasyDeployment deployment = (ResteasyDeployment) di.getServletContextAttributes().get(ResteasyDeployment.class.getName());
+        sessionFactory = ((KeycloakApplication) deployment.getApplication()).getSessionFactory();
 
-            setupDevConfig();
+        setupDevConfig();
 
-            log.info("Auth server started in " + (System.currentTimeMillis() - start) + " ms\n");
-        } finally {
-            setRouteProperty.revert();
-        }
+        log.info("Auth server started in " + (System.currentTimeMillis() - start) + " ms\n");
     }
 
 
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
index bdf0ff7..6caca66 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
@@ -17,6 +17,11 @@
 
 package org.keycloak.testsuite.arquillian.undertow;
 
+import org.keycloak.util.JsonSerialization;
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
 import org.arquillian.undertow.UndertowContainerConfiguration;
 import org.jboss.arquillian.container.spi.ConfigurationException;
 import org.jboss.logging.Logger;
@@ -29,6 +34,8 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat
     private String resourcesHome;
     private boolean remoteMode;
     private String route;
+    private String keycloakConfigPropertyOverrides;
+    private Map<String, String> keycloakConfigPropertyOverridesMap;
 
     private int bindHttpPortOffset = 0;
 
@@ -72,6 +79,18 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat
         this.remoteMode = remoteMode;
     }
 
+    public String getKeycloakConfigPropertyOverrides() {
+        return keycloakConfigPropertyOverrides;
+    }
+
+    public void setKeycloakConfigPropertyOverrides(String keycloakConfigPropertyOverrides) {
+        this.keycloakConfigPropertyOverrides = keycloakConfigPropertyOverrides;
+    }
+
+    public Map<String, String> getKeycloakConfigPropertyOverridesMap() {
+        return keycloakConfigPropertyOverridesMap;
+    }
+
     @Override
     public void validate() throws ConfigurationException {
         super.validate();
@@ -80,6 +99,15 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat
         int newPort = basePort + bindHttpPortOffset;
         setBindHttpPort(newPort);
         log.info("KeycloakOnUndertow will listen on port: " + newPort);
+        
+        if (this.keycloakConfigPropertyOverrides != null) {
+            try {
+                TypeReference<HashMap<String,Object>> typeRef = new TypeReference<HashMap<String,Object>>() {};
+                this.keycloakConfigPropertyOverridesMap = JsonSerialization.sysPropertiesAwareMapper.readValue(this.keycloakConfigPropertyOverrides, typeRef);
+            } catch (IOException ex) {
+                throw new ConfigurationException(ex);
+            }
+        }
 
         // TODO validate workerThreads
         
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
index 3eda20c..3b533f1 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
@@ -18,7 +18,6 @@
 package org.keycloak.testsuite.arquillian.undertow.lb;
 
 import java.net.URI;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -36,6 +35,8 @@ import io.undertow.util.AttachmentKey;
 import io.undertow.util.Headers;
 import org.jboss.logging.Logger;
 import org.keycloak.services.managers.AuthenticationSessionManager;
+import java.util.LinkedHashMap;
+import java.util.StringTokenizer;
 
 /**
  * Loadbalancer on embedded undertow. Supports sticky session over "AUTH_SESSION_ID" cookie and failover to different node when sticky node not available.
@@ -53,8 +54,9 @@ public class SimpleUndertowLoadBalancer {
 
     private final String host;
     private final int port;
-    private final String nodesString;
+    private final Map<String, URI> backendNodes;
     private Undertow undertow;
+    private LoadBalancingProxyClient lb;
 
 
     public static void main(String[] args) throws Exception {
@@ -77,15 +79,14 @@ public class SimpleUndertowLoadBalancer {
     public SimpleUndertowLoadBalancer(String host, int port, String nodesString) {
         this.host = host;
         this.port = port;
-        this.nodesString = nodesString;
-        log.infof("Keycloak nodes: %s", nodesString);
+        this.backendNodes = parseNodes(nodesString);
+        log.infof("Keycloak nodes: %s", backendNodes);
     }
 
 
     public void start() {
-        Map<String, String> nodes = parseNodes(nodesString);
         try {
-            HttpHandler proxyHandler = createHandler(nodes);
+            HttpHandler proxyHandler = createHandler();
 
             undertow = Undertow.builder()
                     .addHttpListener(port, host)
@@ -104,24 +105,51 @@ public class SimpleUndertowLoadBalancer {
         undertow.stop();
     }
 
+    public void enableAllBackendNodes() {
+        backendNodes.forEach((route, uri) -> {
+            lb.removeHost(uri);
+            lb.addHost(uri, route);
+        });
+    }
 
-    static Map<String, String> parseNodes(String nodes) {
-        String[] nodesArray = nodes.split(",");
-        Map<String, String> result = new HashMap<>();
+    public void disableAllBackendNodes() {
+        log.debugf("Load balancer: disabling all nodes");
+        backendNodes.values().forEach(lb::removeHost);
+    }
 
-        for (String nodeStr : nodesArray) {
-            String[] node = nodeStr.trim().split("=");
-            if (node.length != 2) {
-                throw new IllegalArgumentException("Illegal node format in the configuration: " + nodeStr);
-            }
-            result.put(node[0].trim(), node[1].trim());
+    public void enableBackendNodeByName(String nodeName) {
+        URI uri = backendNodes.get(nodeName);
+        if (uri == null) {
+            throw new IllegalArgumentException("Invalid node: " + nodeName);
+        }
+        log.debugf("Load balancer: enabling node %s", nodeName);
+        lb.addHost(uri, nodeName);
+    }
+
+    public void disableBackendNodeByName(String nodeName) {
+        URI uri = backendNodes.get(nodeName);
+        if (uri == null) {
+            throw new IllegalArgumentException("Invalid node: " + nodeName);
+        }
+        log.debugf("Load balancer: disabling node %s", nodeName);
+        lb.removeHost(uri);
+    }
+
+    static Map<String, URI> parseNodes(String nodes) {
+        StringTokenizer st = new StringTokenizer(nodes, ",");
+        Map<String, URI> result = new LinkedHashMap<>();
+
+        while (st.hasMoreElements()) {
+            String nodeStr = st.nextToken();
+            String[] node = nodeStr.trim().split("=", 2);
+            result.put(node[0].trim(), URI.create(node[1].trim()));
         }
 
         return result;
     }
 
 
-    private HttpHandler createHandler(Map<String, String> backendNodes) throws Exception {
+    private HttpHandler createHandler() throws Exception {
 
         // TODO: configurable options if needed
         String sessionCookieNames = AuthenticationSessionManager.AUTH_SESSION_ID;
@@ -133,15 +161,7 @@ public class SimpleUndertowLoadBalancer {
         int connectionIdleTimeout = 60;
         int maxRetryAttempts = backendNodes.size() - 1;
 
-        final LoadBalancingProxyClient lb = new CustomLoadBalancingClient(new ExclusivityChecker() {
-
-            @Override
-            public boolean isExclusivityRequired(HttpServerExchange exchange) {
-                //we always create a new connection for upgrade requests
-                return exchange.getRequestHeaders().contains(Headers.UPGRADE);
-            }
-
-        }, maxRetryAttempts)
+        lb = new CustomLoadBalancingClient(exchange -> exchange.getRequestHeaders().contains(Headers.UPGRADE), maxRetryAttempts)
                 .setConnectionsPerThread(connectionsPerThread)
                 .setMaxQueueSize(requestQueueSize)
                 .setSoftMaxConnectionsPerThread(cachedConnectionsPerThread)
@@ -152,13 +172,10 @@ public class SimpleUndertowLoadBalancer {
             lb.addSessionCookieName(id);
         }
 
-        for (Map.Entry<String, String> node : backendNodes.entrySet()) {
-            String route = node.getKey();
-            URI uri = new URI(node.getValue());
-
+        backendNodes.forEach((route, uri) -> {
             lb.addHost(uri, route);
-            log.infof("Added host: %s, route: %s", uri.toString(), route);
-        }
+            log.debugf("Added host: %s, route: %s", uri.toString(), route);
+        });
 
         ProxyHandler handler = new ProxyHandler(lb, maxTime, ResponseCodeHandler.HANDLE_404);
         return handler;
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java
index 3a0312c..12d3564 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java
@@ -19,13 +19,17 @@ package org.keycloak.testsuite.arquillian.undertow.lb;
 
 import org.arquillian.undertow.UndertowContainerConfiguration;
 import org.jboss.arquillian.container.spi.ConfigurationException;
+import org.jboss.logging.Logger;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerConfiguration {
 
+    protected static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerConfiguration.class);
+
     private String nodes = SimpleUndertowLoadBalancer.DEFAULT_NODES;
+    private int bindHttpPortOffset = 0;
 
     public String getNodes() {
         return nodes;
@@ -35,6 +39,14 @@ public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerCo
         this.nodes = nodes;
     }
 
+    public int getBindHttpPortOffset() {
+        return bindHttpPortOffset;
+    }
+
+    public void setBindHttpPortOffset(int bindHttpPortOffset) {
+        this.bindHttpPortOffset = bindHttpPortOffset;
+    }
+
     @Override
     public void validate() throws ConfigurationException {
         super.validate();
@@ -44,5 +56,11 @@ public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerCo
         } catch (Exception e) {
             throw new ConfigurationException(e);
         }
+
+        int basePort = getBindHttpPort();
+        int newPort = basePort + bindHttpPortOffset;
+        setBindHttpPort(newPort);
+        log.info("SimpleUndertowLoadBalancer will listen on port: " + newPort);
+
     }
 }
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java
index 4b24c15..a7f1ffe 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java
@@ -25,13 +25,14 @@ import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaD
 import org.jboss.logging.Logger;
 import org.jboss.shrinkwrap.api.Archive;
 import org.jboss.shrinkwrap.descriptor.api.Descriptor;
+import org.keycloak.testsuite.arquillian.LoadBalancerController;
 
 /**
  * Arquillian container over {@link SimpleUndertowLoadBalancer}
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class SimpleUndertowLoadBalancerContainer implements DeployableContainer<SimpleUndertowLoadBalancerConfiguration> {
+public class SimpleUndertowLoadBalancerContainer implements DeployableContainer<SimpleUndertowLoadBalancerConfiguration>, LoadBalancerController {
 
     private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerContainer.class);
 
@@ -84,4 +85,24 @@ public class SimpleUndertowLoadBalancerContainer implements DeployableContainer<
     public void undeploy(Descriptor descriptor) throws DeploymentException {
         throw new UnsupportedOperationException("Not implemented");
     }
+
+    @Override
+    public void enableAllBackendNodes() {
+        this.container.enableAllBackendNodes();
+    }
+
+    @Override
+    public void disableAllBackendNodes() {
+        this.container.disableAllBackendNodes();
+    }
+
+    @Override
+    public void enableBackendNodeByName(String nodeName) {
+        this.container.enableBackendNodeByName(nodeName);
+    }
+
+    @Override
+    public void disableBackendNodeByName(String nodeName) {
+        this.container.disableBackendNodeByName(nodeName);
+    }
 }
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java
index 86a7e2e..e64b1b2 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java
@@ -17,13 +17,15 @@
 
 package org.keycloak.testsuite.arquillian.undertow;
 
+import java.io.Closeable;
+
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-class SetSystemProperty {
+class SetSystemProperty implements Closeable {
 
-    private String name;
-    private String oldValue;
+    private final String name;
+    private final String oldValue;
 
     public SetSystemProperty(String name, String value) {
         this.name = name;
@@ -50,4 +52,9 @@ class SetSystemProperty {
         }
     }
 
+    @Override
+    public void close() {
+        revert();
+    }
+
 }
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml
new file mode 100644
index 0000000..d623853
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/assembly.xml
@@ -0,0 +1,46 @@
+<!--
+  ~ 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.
+  -->
+
+<assembly>
+    
+    <id>${cache.server.jboss}</id>
+    
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <includeBaseDirectory>false</includeBaseDirectory>
+
+    <fileSets>
+        <fileSet>
+            <directory>${cache.server.jboss.home}</directory>
+            <outputDirectory>cache-server-${cache.server}</outputDirectory>
+            <excludes>
+                <exclude>**/*.sh</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>${cache.server.jboss.home}</directory>
+            <outputDirectory>cache-server-${cache.server}</outputDirectory>
+            <includes>
+                <include>**/*.sh</include>
+            </includes>
+            <fileMode>0755</fileMode>
+        </fileSet>
+    </fileSets>
+
+</assembly>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
new file mode 100644
index 0000000..ee9c29d
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -0,0 +1,50 @@
+<!--
+  ~ 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.
+  -->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:xalan="http://xml.apache.org/xalan"
+                version="2.0"
+                exclude-result-prefixes="xalan">
+
+    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+    <xsl:strip-space elements="*"/>
+
+    <xsl:variable name="nsCacheServer" select="'urn:infinispan:server:core:'"/>
+
+    <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsCacheServer)]
+                        /*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='local']">
+        <xsl:copy>
+            <xsl:apply-templates select="@* | node()" />
+            <local-cache name="work" start="EAGER" batching="false" />
+        </xsl:copy>
+    </xsl:template>
+
+    <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsCacheServer)]
+                        /*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='clustered']">
+        <xsl:copy>
+            <xsl:apply-templates select="@* | node()" />
+            <replicated-cache name="work" start="EAGER" batching="false" />
+        </xsl:copy>
+    </xsl:template>
+
+    <xsl:template match="@*|node()">
+        <xsl:copy>
+            <xsl:apply-templates select="@*|node()" />
+        </xsl:copy>
+    </xsl:template>
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl
new file mode 100644
index 0000000..03d518a
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/io.xsl
@@ -0,0 +1,40 @@
+<!--
+~ 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.
+-->
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:xalan="http://xml.apache.org/xalan"
+                version="2.0"
+                exclude-result-prefixes="xalan">
+
+    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+    <xsl:strip-space elements="*"/>
+
+    <xsl:param name="worker.io-threads" select="'16'"/>
+    <xsl:param name="worker.task-max-threads" select="'128'"/>
+    
+    <!--set worker threads-->
+    <xsl:template match="//*[local-name()='worker' and @name='default']">
+        <worker name="default" io-threads="{$worker.io-threads}" task-max-threads="{$worker.task-max-threads}" />
+    </xsl:template>
+
+    <xsl:template match="@*|node()">
+        <xsl:copy>
+            <xsl:apply-templates select="@*|node()" />
+        </xsl:copy>
+    </xsl:template>
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml
new file mode 100644
index 0000000..3ac23ac
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+~ 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.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-servers-cache-server-jboss</artifactId>
+        <version>3.2.0.CR1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-servers-cache-server-infinispan</artifactId>
+    <packaging>pom</packaging>
+    <name>Cache Server - JBoss - Infinispan</name>
+    
+    <properties>
+        <cache.server>infinispan</cache.server>
+        <cache.server.container>cache-server-${cache.server}</cache.server.container>
+        <cache.server.home>${containers.home}/${cache.server.container}</cache.server.home>
+        
+        <cache.server.jboss.groupId>org.infinispan.server</cache.server.jboss.groupId>
+        <cache.server.jboss.artifactId>infinispan-server</cache.server.jboss.artifactId>
+        <cache.server.jboss.version>${infinispan.version}</cache.server.jboss.version>
+        <cache.server.jboss.unpacked.folder.name>${cache.server.jboss.artifactId}-${infinispan.version}</cache.server.jboss.unpacked.folder.name>
+        
+        <cache.server.worker.io-threads>${cache.default.worker.io-threads}</cache.server.worker.io-threads>
+        <cache.server.worker.task-max-threads>${cache.default.worker.task-max-threads}</cache.server.worker.task-max-threads>
+    </properties>
+
+</project>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/src/.dont-delete b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/src/.dont-delete
new file mode 100644
index 0000000..c969aca
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/src/.dont-delete
@@ -0,0 +1 @@
+This file is to mark this Maven project as a valid option for building cache server artifact
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml
new file mode 100644
index 0000000..8fa5902
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+~ 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.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-servers-cache-server-jboss</artifactId>
+        <version>3.2.0.CR1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-servers-cache-server-jdg</artifactId>
+    <packaging>pom</packaging>
+    <name>Cache Server - JDG</name>
+    
+    <properties>
+        <cache.server>jdg</cache.server>
+        <cache.server.container>cache-server-${cache.server}</cache.server.container>
+        <cache.server.home>${containers.home}/${cache.server.container}</cache.server.home>
+        
+        <cache.server.jboss.groupId>org.infinispan.server</cache.server.jboss.groupId>
+        <cache.server.jboss.artifactId>infinispan-server</cache.server.jboss.artifactId>
+        <cache.server.jboss.version>${jdg.version}</cache.server.jboss.version>
+        <cache.server.jboss.unpacked.folder.name>${cache.server.jboss.artifactId}-${jdg.version}</cache.server.jboss.unpacked.folder.name>
+        
+        <cache.server.worker.io-threads>${cache.default.worker.io-threads}</cache.server.worker.io-threads>
+        <cache.server.worker.task-max-threads>${cache.default.worker.task-max-threads}</cache.server.worker.task-max-threads>
+    </properties>
+
+</project>
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/src/.dont-delete b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/src/.dont-delete
new file mode 100644
index 0000000..c969aca
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/src/.dont-delete
@@ -0,0 +1 @@
+This file is to mark this Maven project as a valid option for building cache server artifact
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
new file mode 100644
index 0000000..9c2d1f9
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0"?>
+<!--
+~ 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.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-servers-cache-server</artifactId>
+        <version>3.2.0.CR1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-servers-cache-server-jboss</artifactId>
+    <packaging>pom</packaging>
+    <name>Cache Server - JBoss Family</name>
+
+    <properties>
+        <common.resources>${project.parent.basedir}/common</common.resources>
+        <assembly.xml>${project.parent.basedir}/assembly.xml</assembly.xml>
+        <cache.server.jboss.home>${containers.home}/${cache.server.jboss.unpacked.folder.name}</cache.server.jboss.home>
+        <security.xslt>security.xsl</security.xslt>
+    </properties>
+
+    <profiles>
+        
+        <profile>
+            <id>cache-server-jboss-submodules</id>
+            <activation>
+                <file>
+                    <exists>src</exists>
+                </file>
+            </activation>
+            <build>
+                <plugins>
+                    
+                    <plugin>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>enforce</goal>
+                                </goals>
+                                <configuration>
+                                    <rules>
+                                        <requireProperty>
+                                            <property>cache.server</property>
+                                            <property>cache.server.jboss.groupId</property>
+                                            <property>cache.server.jboss.artifactId</property>
+                                            <property>cache.server.jboss.version</property>
+                                            <property>cache.server.jboss.unpacked.folder.name</property>
+                                        </requireProperty>
+                                    </rules>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    
+                    <plugin>
+                        <artifactId>maven-dependency-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>unpack-cache-server</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>unpack</goal>
+                                </goals>
+                                <configuration>
+                                    <artifactItems>
+                                        <artifactItem>
+                                            <groupId>${cache.server.jboss.groupId}</groupId>
+                                            <artifactId>${cache.server.jboss.artifactId}</artifactId>
+                                            <version>${cache.server.jboss.version}</version>
+                                            <type>zip</type>
+                                            <classifier>bin</classifier>
+                                            <outputDirectory>${containers.home}</outputDirectory>
+                                        </artifactItem>
+                                    </artifactItems>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+            
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>xml-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>configure-keycloak-caches</id>
+                                <phase>process-test-resources</phase>
+                                <goals>
+                                    <goal>transform</goal>
+                                </goals>
+                                <configuration>
+                                    <transformationSets>
+                                        <transformationSet>
+                                            <dir>${cache.server.jboss.home}/standalone/configuration</dir>
+                                            <includes>
+                                                <include>standalone.xml</include>
+                                                <include>clustered.xml</include>
+                                            </includes>
+                                            <stylesheet>${common.resources}/add-keycloak-caches.xsl</stylesheet>
+                                            <outputDir>${cache.server.jboss.home}/standalone/configuration</outputDir>
+                                        </transformationSet>
+                                    </transformationSets>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>io-worker-threads</id>
+                                <phase>process-resources</phase>
+                                <goals>
+                                    <goal>transform</goal>
+                                </goals>
+                                <configuration>
+                                    <transformationSets>
+                                        <transformationSet>
+                                            <dir>${cache.server.jboss.home}/standalone/configuration</dir>
+                                            <includes>
+                                                <include>standalone.xml</include>
+                                                <include>standalone-ha.xml</include>
+                                            </includes>
+                                            <stylesheet>${common.resources}/io.xsl</stylesheet>
+                                            <outputDir>${cache.server.jboss.home}/standalone/configuration</outputDir>
+                                            <parameters>
+                                                <parameter>
+                                                    <name>worker.io-threads</name>
+                                                    <value>${cache.server.worker.io-threads}</value>
+                                                </parameter>
+                                                <parameter>
+                                                    <name>worker.task-max-threads</name>
+                                                    <value>${cache.server.worker.task-max-threads}</value>
+                                                </parameter>
+                                            </parameters>
+                                        </transformationSet>
+                                    </transformationSets>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    
+                    <plugin>
+                        <artifactId>maven-resources-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>enable-jboss-mgmt-admin</id>
+                                <phase>process-resources</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${cache.server.jboss.home}/standalone/configuration</outputDirectory>
+                                    <resources>
+                                        <resource>
+                                            <directory>${common.resources}</directory>
+                                            <includes>
+                                                <include>mgmt-users.properties</include>
+                                            </includes>
+                                        </resource>
+                                    </resources>
+                                    <overwrite>true</overwrite>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>copy-cache-server-configuration-for-dc-2</id>
+                                <phase>process-resources</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${cache.server.jboss.home}/standalone-dc-2/deployments</outputDirectory>
+                                    <includeEmptyDirs>true</includeEmptyDirs>
+                                    <resources>
+                                        <resource>
+                                            <directory>${cache.server.jboss.home}/standalone/deployments</directory>
+                                        </resource>
+                                    </resources>
+                                    <overwrite>true</overwrite>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>create-zip</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <configuration>
+                                    <descriptors>
+                                        <descriptor>${assembly.xml}</descriptor>
+                                    </descriptors>
+                                    <appendAssemblyId>false</appendAssemblyId>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    
+                </plugins>
+            </build>
+        </profile>
+        
+        <profile>
+            <id>cache-server-infinispan</id>
+            <modules>
+                <module>infinispan</module>
+            </modules>
+        </profile>
+        <profile>
+            <id>cache-server-jdg</id>
+            <modules>
+                <module>jdg</module>
+            </modules>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/testsuite/integration-arquillian/servers/cache-server/pom.xml b/testsuite/integration-arquillian/servers/cache-server/pom.xml
new file mode 100644
index 0000000..3f5a5a0
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/cache-server/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+~ 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.
+-->
+
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-servers</artifactId>
+        <version>3.2.0.CR1-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-servers-cache-server</artifactId>
+    <packaging>pom</packaging>
+    <name>Cache Server</name>
+
+    <properties>
+        <auth.server.worker.io-threads>${jboss.default.worker.io-threads}</auth.server.worker.io-threads>
+        <auth.server.worker.task-max-threads>${jboss.default.worker.task-max-threads}</auth.server.worker.task-max-threads>
+    </properties>
+
+    <modules>
+        <module>jboss</module>
+    </modules>
+
+</project>
diff --git a/testsuite/integration-arquillian/servers/pom.xml b/testsuite/integration-arquillian/servers/pom.xml
index 96d6c18..0b7e899 100644
--- a/testsuite/integration-arquillian/servers/pom.xml
+++ b/testsuite/integration-arquillian/servers/pom.xml
@@ -46,13 +46,21 @@
         <!--<fuse62.version>6.2.0.redhat-133</fuse62.version>-->
         <fuse62.version>6.2.1.redhat-084</fuse62.version>
         
+        <!-- cache server versions -->
+        <infinispan.version>9.0.1.Final</infinispan.version>
+        <jdg.version>8.4.0.Final-redhat-2</jdg.version><!-- JDG 7.1.0 -->
+        
         <jboss.default.worker.io-threads>16</jboss.default.worker.io-threads>
         <jboss.default.worker.task-max-threads>128</jboss.default.worker.task-max-threads>
+
+        <cache.default.worker.io-threads>2</cache.default.worker.io-threads>
+        <cache.default.worker.task-max-threads>4</cache.default.worker.task-max-threads>
     </properties>
 
     <modules>
         <module>auth-server</module>
         <module>app-server</module>
+        <module>cache-server</module>
     </modules>
 
     <profiles>
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 7c52025..caa76aa 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -40,6 +40,7 @@
         <exclude.client>-</exclude.client>
         <!--exclude cluster tests by default, enabled by 'auth-server-*-cluster' profiles in tests/pom.xml-->
         <exclude.cluster>**/cluster/**/*Test.java</exclude.cluster>
+        <exclude.crossdc>**/crossdc/**/*Test.java</exclude.crossdc>
         <!-- exclude x509 tests by default, enabled by 'ssl' profile -->
         <exclude.x509>**/x509/*Test.java</exclude.x509>
         <!-- exclude undertow adapter tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
@@ -125,6 +126,7 @@
                         <exclude>${exclude.account}</exclude>
                         <exclude>${exclude.client}</exclude>
                         <exclude>${exclude.cluster}</exclude>
+                        <exclude>${exclude.crossdc}</exclude>
                         <exclude>${exclude.undertow.adapter}</exclude>
                         <exclude>${exclude.x509}</exclude>
                     </excludes>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAutodetectServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAutodetectServlet.java
new file mode 100644
index 0000000..9e7e3b7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAutodetectServlet.java
@@ -0,0 +1,39 @@
+/*
+ * 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.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+
+import java.net.URL;
+
+/**
+ * @author mhajas
+ */
+public class SalesPostAutodetectServlet extends SAMLServlet {
+    public static final String DEPLOYMENT_NAME = "sales-post-autodetect";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
new file mode 100644
index 0000000..2dd7bbc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 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.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for a field / method parameter annotating {@link InfinispanStatistics} object that would be used
+ * to access statistics via JMX. By default, the access to "work" cache at remote infinispan / JDG server is requested
+ * yet the same annotation is used for other caches as well.
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanCacheStatistics {
+
+    /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+    String domain() default "";
+
+    // JMX address properties
+    String type() default Constants.TYPE_CACHE;
+    String cacheName() default "work";
+    String cacheMode() default "*";
+    String cacheManagerName() default "*";
+    String component() default Constants.COMPONENT_STATISTICS;
+
+    // Host address - either given by arrangement of DC ...
+
+    /** Index of the data center, starting from 0 */
+    int dcIndex() default -1;
+    /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+    int dcNodeIndex() default -1;
+
+    // ... or by specific host/port
+
+    /** Port for management */
+    int managementPort() default -1;
+    /** Name of system property to obtain management port from */
+    String managementPortProperty() default "";
+    /** Host name to connect to */
+    String host() default "";
+    /** Name of system property to obtain host name from */
+    String hostProperty() default "";
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
new file mode 100644
index 0000000..41e9f20
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 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.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanChannelStatistics {
+
+    /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+    String domain() default "";
+
+    // JMX address properties
+    String type() default Constants.TYPE_CHANNEL;
+    String cluster() default "*";
+
+    // Host address - either given by arrangement of DC ...
+
+    /** Index of the data center, starting from 0 */
+    int dcIndex() default -1;
+    /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+    int dcNodeIndex() default -1;
+
+    /** Port for management */
+    int managementPort() default -1;
+    /** Name of system property to obtain management port from */
+    String managementPortProperty() default "";
+    /** Host name to connect to */
+    String host() default "";
+    /** Name of system property to obtain host name from */
+    String hostProperty() default "";
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java
new file mode 100644
index 0000000..a3652a1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/LoadBalancer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 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.arquillian.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD })
+public @interface LoadBalancer {
+    String value() default "";
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index 7e7ee6f..97347d9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -16,7 +16,6 @@
  */
 package org.keycloak.testsuite.arquillian;
 
-import org.jboss.arquillian.container.spi.Container;
 import org.jboss.arquillian.container.spi.ContainerRegistry;
 import org.jboss.arquillian.container.spi.event.StartContainer;
 import org.jboss.arquillian.container.spi.event.StartSuiteContainers;
@@ -41,10 +40,11 @@ import org.keycloak.testsuite.util.OAuthClient;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
+import java.util.stream.Collectors;
 import javax.ws.rs.NotFoundException;
 
 /**
@@ -68,8 +68,18 @@ public class AuthServerTestEnricher {
     private static final String AUTH_SERVER_CONTAINER_PROPERTY = "auth.server.container";
     public static final String AUTH_SERVER_CONTAINER = System.getProperty(AUTH_SERVER_CONTAINER_PROPERTY, AUTH_SERVER_CONTAINER_DEFAULT);
 
+    private static final String AUTH_SERVER_BACKEND_DEFAULT = AUTH_SERVER_CONTAINER + "-backend";
+    private static final String AUTH_SERVER_BACKEND_PROPERTY = "auth.server.backend";
+    public static final String AUTH_SERVER_BACKEND = System.getProperty(AUTH_SERVER_BACKEND_PROPERTY, AUTH_SERVER_BACKEND_DEFAULT);
+
+    private static final String AUTH_SERVER_BALANCER_DEFAULT = "auth-server-balancer";
+    private static final String AUTH_SERVER_BALANCER_PROPERTY = "auth.server.balancer";
+    public static final String AUTH_SERVER_BALANCER = System.getProperty(AUTH_SERVER_BALANCER_PROPERTY, AUTH_SERVER_BALANCER_DEFAULT);
+
     private static final String AUTH_SERVER_CLUSTER_PROPERTY = "auth.server.cluster";
     public static final boolean AUTH_SERVER_CLUSTER = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CLUSTER_PROPERTY, "false"));
+    private static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc";
+    public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false"));
 
     private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false"));
 
@@ -106,46 +116,83 @@ public class AuthServerTestEnricher {
     }
 
     public void initializeSuiteContext(@Observes(precedence = 2) BeforeSuite event) {
-
-        Set<ContainerInfo> containers = new LinkedHashSet<>();
-        for (Container c : containerRegistry.get().getContainers()) {
-            containers.add(new ContainerInfo(c));
-        }
+        Set<ContainerInfo> containers = containerRegistry.get().getContainers().stream()
+          .map(ContainerInfo::new)
+          .collect(Collectors.toSet());
 
         suiteContext = new SuiteContext(containers);
 
-        String authServerFrontend = null;
-
-        if (AUTH_SERVER_CLUSTER) {
-            // if cluster mode enabled, load-balancer is the frontend
-            for (ContainerInfo c : containers) {
-                if (c.getQualifier().startsWith("auth-server-balancer")) {
-                    authServerFrontend = c.getQualifier();
-                }
+        if (AUTH_SERVER_CROSS_DC) {
+            // if cross-dc mode enabled, load-balancer is the frontend of datacenter cluster
+            containers.stream()
+              .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BALANCER + "-cross-dc"))
+              .forEach(c -> {
+                String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0");
+                String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0");
+                updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString));
+                suiteContext.addAuthServerInfo(Integer.valueOf(dcString), c);
+              });
+
+            if (suiteContext.getDcAuthServerInfo().isEmpty()) {
+                throw new IllegalStateException("Not found frontend container (load balancer): " + AUTH_SERVER_BALANCER);
             }
-
-            if (authServerFrontend != null) {
-                log.info("Using frontend container: " + authServerFrontend);
-            } else {
-                throw new IllegalStateException("Not found frontend container");
+            if (suiteContext.getDcAuthServerInfo().stream().anyMatch(Objects::isNull)) {
+                throw new IllegalStateException("Frontend container (load balancer) misconfiguration");
             }
-        } else {
-            authServerFrontend = AUTH_SERVER_CONTAINER; // single-node mode
-        }
 
-        String authServerBackend = AUTH_SERVER_CONTAINER + "-backend";
-        int backends = 0;
-        for (ContainerInfo container : suiteContext.getContainers()) {
-            // frontend
-            if (container.getQualifier().equals(authServerFrontend)) {
-                updateWithAuthServerInfo(container);
-                suiteContext.setAuthServerInfo(container);
+            containers.stream()
+              .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER + "-cross-dc-"))
+              .sorted((a, b) -> a.getQualifier().compareTo(b.getQualifier()))
+              .forEach(c -> {
+                String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0");
+                String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0");
+                updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString));
+                suiteContext.addAuthServerBackendsInfo(Integer.valueOf(dcString), c);
+              });
+
+            if (suiteContext.getDcAuthServerInfo().isEmpty()) {
+                throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", AUTH_SERVER_BACKEND));
             }
-            // backends
-            if (AUTH_SERVER_CLUSTER && container.getQualifier().startsWith(authServerBackend)) {
-                updateWithAuthServerInfo(container, ++backends);
-                suiteContext.getAuthServerBackendsInfo().add(container);
+            if (suiteContext.getDcAuthServerBackendsInfo().stream().anyMatch(Objects::isNull)) {
+                throw new IllegalStateException("Frontend container (load balancer) misconfiguration");
             }
+            if (suiteContext.getDcAuthServerBackendsInfo().stream().anyMatch(List::isEmpty)) {
+                throw new RuntimeException(String.format("Some data center has no auth server container matching '%s' defined in arquillian.xml.", AUTH_SERVER_BACKEND));
+            }
+
+            log.info("Using frontend containers: " + this.suiteContext.getDcAuthServerInfo().stream()
+              .map(ContainerInfo::getQualifier)
+              .collect(Collectors.joining(", ")));
+        } else if (AUTH_SERVER_CLUSTER) {
+            // if cluster mode enabled, load-balancer is the frontend
+            ContainerInfo container = containers.stream()
+              .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BALANCER))
+              .findAny()
+              .orElseThrow(() -> new IllegalStateException("Not found frontend container: " + AUTH_SERVER_BALANCER));
+            updateWithAuthServerInfo(container);
+            suiteContext.setAuthServerInfo(container);
+
+            containers.stream()
+              .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_BACKEND))
+              .forEach(c -> {
+                String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0");
+                updateWithAuthServerInfo(c, Integer.valueOf(portOffsetString));
+                suiteContext.addAuthServerBackendsInfo(0, c);
+              });
+
+            if (suiteContext.getAuthServerBackendsInfo().isEmpty()) {
+                throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", AUTH_SERVER_BACKEND));
+            }
+
+            log.info("Using frontend container: " + container.getQualifier());
+        } else {
+            // frontend-only
+            ContainerInfo container = containers.stream()
+              .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER))
+              .findAny()
+              .orElseThrow(() -> new IllegalStateException("Not found frontend container: " + AUTH_SERVER_CONTAINER));
+            updateWithAuthServerInfo(container);
+            suiteContext.setAuthServerInfo(container);
         }
 
         // Setup with 2 undertow backend nodes and no loadbalancer.
@@ -153,14 +200,6 @@ public class AuthServerTestEnricher {
 //            suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0));
 //        }
 
-        // validate auth server setup
-        if (suiteContext.getAuthServerInfo() == null) {
-            throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", authServerFrontend));
-        }
-        if (AUTH_SERVER_CLUSTER && suiteContext.getAuthServerBackendsInfo().isEmpty()) {
-            throw new RuntimeException(String.format("No auth server container matching '%sN' found in arquillian.xml.", authServerBackend));
-        }
-
         if (START_MIGRATION_CONTAINER) {
             // init migratedAuthServerInfo
             for (ContainerInfo container : suiteContext.getContainers()) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
new file mode 100644
index 0000000..4091ca4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
@@ -0,0 +1,356 @@
+package org.keycloak.testsuite.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.testsuite.Retry;
+import java.util.Map;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.container.spi.Container;
+import org.jboss.arquillian.container.spi.ContainerRegistry;
+import org.jboss.arquillian.test.spi.TestEnricher;
+import java.io.IOException;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.management.Attribute;
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.MalformedObjectNameException;
+import javax.management.ReflectionException;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import java.util.Set;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistry;
+import org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow;
+import java.io.NotSerializableException;
+import java.lang.management.ManagementFactory;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.jboss.arquillian.core.spi.Validate;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class CacheStatisticsControllerEnricher implements TestEnricher {
+
+    private static final Logger LOG = Logger.getLogger(CacheStatisticsControllerEnricher.class);
+
+    @Inject
+    private Instance<ContainerRegistry> registry;
+
+    @Inject
+    private Instance<JmxConnectorRegistry> jmxConnectorRegistry;
+
+    @Inject
+    private Instance<SuiteContext> suiteContext;
+
+    @Override
+    public void enrich(Object testCase) {
+        Validate.notNull(registry.get(), "registry should not be null");
+        Validate.notNull(jmxConnectorRegistry.get(), "jmxConnectorRegistry should not be null");
+        Validate.notNull(suiteContext.get(), "suiteContext should not be null");
+
+        for (Field field : FieldUtils.getAllFields(testCase.getClass())) {
+            JmxInfinispanCacheStatistics annotation = field.getAnnotation(JmxInfinispanCacheStatistics.class);
+
+            if (annotation == null) {
+                continue;
+            }
+
+            try {
+                FieldUtils.writeField(field, testCase, getInfinispanCacheStatistics(annotation), true);
+            } catch (IOException | IllegalAccessException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + field);
+            }
+        }
+    }
+
+    private InfinispanStatistics getInfinispanCacheStatistics(JmxInfinispanCacheStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+        MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+        ObjectName mbeanName = new ObjectName(String.format(
+          "%s:type=%s,name=\"%s(%s)\",manager=\"%s\",component=%s",
+          annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+          annotation.type(),
+          annotation.cacheName(),
+          annotation.cacheMode(),
+          annotation.cacheManagerName(),
+          annotation.component()
+        ));
+
+        InfinispanStatistics value = new InfinispanCacheStatisticsImpl(mbsc, mbeanName);
+
+        if (annotation.domain().isEmpty()) {
+            try {
+                Retry.execute(() -> value.reset(), 2, 150);
+            } catch (RuntimeException ex) {
+                if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+                   && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+                    LOG.warn("Could not reset statistics for " + mbeanName);
+                }
+            }
+        }
+
+        return value;
+    }
+
+    private InfinispanStatistics getJGroupsChannelStatistics(JmxInfinispanChannelStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+        MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+        ObjectName mbeanName = new ObjectName(String.format(
+          "%s:type=%s,cluster=\"%s\"",
+          annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+          annotation.type(),
+          annotation.cluster()
+        ));
+
+        InfinispanStatistics value = new InfinispanChannelStatisticsImpl(mbsc, mbeanName);
+
+        if (annotation.domain().isEmpty()) {
+            try {
+                Retry.execute(() -> value.reset(), 2, 150);
+            } catch (RuntimeException ex) {
+                if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+                   && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+                    LOG.warn("Could not reset statistics for " + mbeanName);
+                }
+            }
+        }
+
+        return value;
+    }
+
+    @Override
+    public Object[] resolve(Method method) {
+        Object[] values = new Object[method.getParameterCount()];
+
+        for (int i = 0; i < method.getParameterCount(); i ++) {
+            Parameter param = method.getParameters()[i];
+
+            JmxInfinispanCacheStatistics annotation = param.getAnnotation(JmxInfinispanCacheStatistics.class);
+            if (annotation != null) try {
+                values[i] = getInfinispanCacheStatistics(annotation);
+            } catch (IOException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + param);
+            }
+
+            JmxInfinispanChannelStatistics channelAnnotation = param.getAnnotation(JmxInfinispanChannelStatistics.class);
+            if (channelAnnotation != null) try {
+                values[i] = getJGroupsChannelStatistics(channelAnnotation);
+            } catch (IOException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + param);
+            }
+        }
+
+        return values;
+    }
+
+    private String getDefaultDomain(int dcIndex, int dcNodeIndex) {
+        if (dcIndex != -1 && dcNodeIndex != -1) {
+            return InfinispanConnectionProvider.JMX_DOMAIN + "-" + suiteContext.get().getAuthServerBackendsInfo(dcIndex).get(dcNodeIndex).getQualifier();
+        }
+        return InfinispanConnectionProvider.JMX_DOMAIN;
+    }
+
+    private MBeanServerConnection getJmxServerConnection(JmxInfinispanCacheStatistics annotation) throws MalformedURLException, IOException {
+        final String host;
+        final int port;
+
+        if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+            ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+            Container container = node.getArquillianContainer();
+            if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+                return ManagementFactory.getPlatformMBeanServer();
+            }
+            host = "localhost";
+            port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+              ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+              : 9990;
+        } else {
+            host = annotation.host().isEmpty()
+              ? System.getProperty((annotation.hostProperty().isEmpty()
+                ? "keycloak.connectionsInfinispan.remoteStoreServer"
+                : annotation.hostProperty()))
+              : annotation.host();
+
+            port = annotation.managementPort() == -1
+              ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+                ? "cache.server.management.port"
+                : annotation.managementPortProperty())))
+              : annotation.managementPort();
+        }
+
+        JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+        JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+        return jmxc.getMBeanServerConnection();
+    }
+
+    private MBeanServerConnection getJmxServerConnection(JmxInfinispanChannelStatistics annotation) throws MalformedURLException, IOException {
+        final String host;
+        final int port;
+
+        if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+            ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+            Container container = node.getArquillianContainer();
+            if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+                return ManagementFactory.getPlatformMBeanServer();
+            }
+            host = "localhost";
+            port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+              ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+              : 9990;
+        } else {
+            host = annotation.host().isEmpty()
+              ? System.getProperty((annotation.hostProperty().isEmpty()
+                ? "keycloak.connectionsInfinispan.remoteStoreServer"
+                : annotation.hostProperty()))
+              : annotation.host();
+
+            port = annotation.managementPort() == -1
+              ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+                ? "cache.server.management.port"
+                : annotation.managementPortProperty())))
+              : annotation.managementPort();
+        }
+
+        JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+        JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+        return jmxc.getMBeanServerConnection();
+    }
+
+    private static abstract class CacheStatisticsImpl implements InfinispanStatistics {
+
+        protected final MBeanServerConnection mbsc;
+        private final ObjectName mbeanNameTemplate;
+        private ObjectName mbeanName;
+
+        public CacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanNameTemplate) {
+            this.mbsc = mbsc;
+            this.mbeanNameTemplate = mbeanNameTemplate;
+        }
+
+        @Override
+        public boolean exists() {
+            try {
+                getMbeanName();
+                return true;
+            } catch (Exception ex) {
+                return false;
+            }
+        }
+
+        @Override
+        public Map<String, Object> getStatistics() {
+            try {
+                MBeanInfo mBeanInfo = mbsc.getMBeanInfo(getMbeanName());
+                String[] statAttrs = Arrays.asList(mBeanInfo.getAttributes()).stream()
+                  .filter(MBeanAttributeInfo::isReadable)
+                  .map(MBeanAttributeInfo::getName)
+                  .collect(Collectors.toList())
+                  .toArray(new String[] {});
+                return mbsc.getAttributes(getMbeanName(), statAttrs)
+                  .asList()
+                  .stream()
+                  .collect(Collectors.toMap(Attribute::getName, Attribute::getValue));
+            } catch (IOException | InstanceNotFoundException | ReflectionException | IntrospectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        protected ObjectName getMbeanName() throws IOException, RuntimeException {
+            if (this.mbeanName == null) {
+                Set<ObjectName> queryNames = mbsc.queryNames(mbeanNameTemplate, null);
+                if (queryNames.isEmpty()) {
+                    throw new RuntimeException("No MBean of template " + mbeanNameTemplate + " found at JMX server");
+                }
+                this.mbeanName = queryNames.iterator().next();
+            }
+
+            return this.mbeanName;
+        }
+
+        @Override
+        public Comparable getSingleStatistics(String statisticsName) {
+            try {
+                return (Comparable) mbsc.getAttribute(getMbeanName(), statisticsName);
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException | AttributeNotFoundException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        public void waitToBecomeAvailable(int time, TimeUnit unit) {
+            long timeInMillis = TimeUnit.MILLISECONDS.convert(time, unit);
+            Retry.execute(() -> {
+                try {
+                    getMbeanName();
+                    if (! isAvailable()) throw new RuntimeException("Not available");
+                } catch (Exception ex) {
+                    throw new RuntimeException("Timed out while waiting for " + mbeanNameTemplate + " to become available", ex);
+                }
+            }, 1 + (int) timeInMillis / 100, 100);
+        }
+
+        protected abstract boolean isAvailable();
+    }
+
+    private static class InfinispanCacheStatisticsImpl extends CacheStatisticsImpl {
+
+        public InfinispanCacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+            super(mbsc, mbeanName);
+        }
+
+        @Override
+        public void reset() {
+            try {
+                mbsc.invoke(getMbeanName(), "resetStatistics", new Object[] {}, new String[] {});
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        protected boolean isAvailable() {
+            return getSingleStatistics(Constants.STAT_CACHE_ELAPSED_TIME) != null;
+        }
+    }
+
+    private static class InfinispanChannelStatisticsImpl extends CacheStatisticsImpl {
+
+        public InfinispanChannelStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+            super(mbsc, mbeanName);
+        }
+
+        @Override
+        public void reset() {
+            try {
+                mbsc.invoke(getMbeanName(), "resetStats", new Object[] {}, new String[] {});
+            } catch (NotSerializableException ex) {
+                // Ignore return value not serializable, the invocation has already done its job
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        protected boolean isAvailable() {
+            return Objects.equals(getSingleStatistics(Constants.STAT_CHANNEL_CONNECTED), Boolean.TRUE);
+       }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
index 68c9f17..37abe7b 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
@@ -5,6 +5,8 @@ import org.jboss.arquillian.container.spi.Container;
 import java.net.URL;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Stream;
+import org.jboss.arquillian.container.spi.Container.State;
 
 /**
  *
@@ -97,4 +99,12 @@ public class ContainerInfo {
                 other.arquillianContainer.getContainerConfiguration().getContainerName());
     }
 
+    public boolean isStarted() {
+        return arquillianContainer.getState() == State.STARTED;
+    }
+
+    public boolean isManual() {
+        return Objects.equals(arquillianContainer.getContainerConfiguration().getMode(), "manual");
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
index 99b6772..41278fc 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
@@ -35,6 +35,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import org.mvel2.MVEL;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.isClassPresent;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.loadClass;
 
@@ -97,10 +98,14 @@ public class RegistryCreator {
 
     private static final String ENABLED = "enabled";
 
-    private boolean isEnabled(ContainerDef containerDef) {
+    private static boolean isEnabled(ContainerDef containerDef) {
         Map<String, String> props = containerDef.getContainerProperties();
-        return !props.containsKey(ENABLED)
-                || (props.containsKey(ENABLED) && props.get(ENABLED).equals("true"));
+        try {
+            return !props.containsKey(ENABLED)
+                    || (props.containsKey(ENABLED) && ! props.get(ENABLED).isEmpty() && MVEL.evalToBoolean(props.get(ENABLED), (Object) null));
+        } catch (Exception ex) {
+            return false;
+        }
     }
 
     @SuppressWarnings("rawtypes")
@@ -110,6 +115,8 @@ public class RegistryCreator {
             if (isClassPresent(getAdapterImplClassValue(containerDef))) {
                 return DeployableContainer.class.isAssignableFrom(
                         loadClass(getAdapterImplClassValue(containerDef)));
+            } else {
+                log.warn("Cannot load adapterImpl class for " + containerDef.getContainerName());
             }
         }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
new file mode 100644
index 0000000..b315937
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 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.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface InfinispanStatistics {
+
+    public static class Constants {
+        public static final String DOMAIN_INFINISPAN_DATAGRID = InfinispanConnectionProvider.JMX_DOMAIN;
+
+        public static final String TYPE_CHANNEL = "channel";
+        public static final String TYPE_CACHE = "Cache";
+        public static final String TYPE_CACHE_MANAGER = "CacheManager";
+
+        public static final String COMPONENT_STATISTICS = "Statistics";
+
+        /** Cache statistics */
+        public static final String STAT_CACHE_AVERAGE_READ_TIME = "averageReadTime";
+        public static final String STAT_CACHE_AVERAGE_WRITE_TIME = "averageWriteTime";
+        public static final String STAT_CACHE_ELAPSED_TIME = "elapsedTime";
+        public static final String STAT_CACHE_EVICTIONS = "evictions";
+        public static final String STAT_CACHE_HITS = "hits";
+        public static final String STAT_CACHE_HIT_RATIO = "hitRatio";
+        public static final String STAT_CACHE_MISSES = "misses";
+        public static final String STAT_CACHE_NUMBER_OF_ENTRIES = "numberOfEntries";
+        public static final String STAT_CACHE_NUMBER_OF_ENTRIES_IN_MEMORY = "numberOfEntriesInMemory";
+        public static final String STAT_CACHE_READ_WRITE_RATIO = "readWriteRatio";
+        public static final String STAT_CACHE_REMOVE_HITS = "removeHits";
+        public static final String STAT_CACHE_REMOVE_MISSES = "removeMisses";
+        public static final String STAT_CACHE_STORES = "stores";
+        public static final String STAT_CACHE_TIME_SINCE_RESET = "timeSinceReset";
+
+        /** JGroups channel statistics */
+        public static final String STAT_CHANNEL_ADDRESS = "address";
+        public static final String STAT_CHANNEL_ADDRESS_UUID = "address_uuid";
+        public static final String STAT_CHANNEL_CLOSED = "closed";
+        public static final String STAT_CHANNEL_CLUSTER_NAME = "cluster_name";
+        public static final String STAT_CHANNEL_CONNECTED = "connected";
+        public static final String STAT_CHANNEL_CONNECTING = "connecting";
+        public static final String STAT_CHANNEL_DISCARD_OWN_MESSAGES = "discard_own_messages";
+        public static final String STAT_CHANNEL_OPEN = "open";
+        public static final String STAT_CHANNEL_RECEIVED_BYTES = "received_bytes";
+        public static final String STAT_CHANNEL_RECEIVED_MESSAGES = "received_messages";
+        public static final String STAT_CHANNEL_SENT_BYTES = "sent_bytes";
+        public static final String STAT_CHANNEL_SENT_MESSAGES = "sent_messages";
+        public static final String STAT_CHANNEL_STATE = "state";
+        public static final String STAT_CHANNEL_STATS = "stats";
+        public static final String STAT_CHANNEL_VIEW = "view";
+
+    }
+
+    Map<String, Object> getStatistics();
+
+    Comparable getSingleStatistics(String statisticsName);
+
+    void waitToBecomeAvailable(int time, TimeUnit unit);
+
+    /**
+     * Resets the statistics counters.
+     */
+    void reset();
+
+    /**
+     * Returns {@code true} iff the statistics represented by this object can be retrieved from the server.
+     */
+    boolean exists();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
new file mode 100644
index 0000000..3a87c5b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.arquillian.jmx;
+
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface JmxConnectorRegistry {
+    JMXConnector getConnection(JMXServiceURL url);
+
+    void closeAll();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
new file mode 100644
index 0000000..50c9b96
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 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.arquillian.jmx;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.core.api.InstanceProducer;
+import org.jboss.arquillian.core.api.annotation.ApplicationScoped;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.core.api.annotation.Observes;
+import org.jboss.arquillian.test.spi.event.suite.BeforeSuite;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class JmxConnectorRegistryCreator {
+
+    @Inject
+    @ApplicationScoped
+    private InstanceProducer<JmxConnectorRegistry> connectorRegistry;
+
+    public void configureJmxConnectorRegistry(@Observes BeforeSuite event) {
+        if (connectorRegistry.get() == null) {
+            connectorRegistry.set(new JmxConnectorRegistry() {
+
+                private volatile ConcurrentMap<JMXServiceURL, JMXConnector> connectors = new ConcurrentHashMap<>();
+
+                @Override
+                public JMXConnector getConnection(JMXServiceURL url) {
+                    JMXConnector res = connectors.get(url);
+                    if (res == null) {
+                        try {
+                            final JMXConnector conn = JMXConnectorFactory.newJMXConnector(url, null);
+                            res = connectors.putIfAbsent(url, conn);
+                            if (res == null) {
+                                res = conn;
+                            }
+                            res.connect();
+                        } catch (IOException ex) {
+                            throw new RuntimeException("Could not instantiate JMX connector for " + url, ex);
+                        }
+                    }
+                    return res;
+                }
+
+                @Override
+                public void closeAll() {
+                    connectors.values().forEach(c -> { try { c.close(); } catch (IOException e) {} });
+                    connectors.clear();
+                }
+            });
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index 7b9c139..33dc8c2 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -32,9 +32,11 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
 import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
 import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
 import org.keycloak.testsuite.arquillian.h2.H2TestEnricher;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistryCreator;
 import org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer;
 import org.keycloak.testsuite.arquillian.migration.MigrationTestExecutionDecider;
 import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
+import org.keycloak.testsuite.arquillian.provider.LoadBalancerControllerProvider;
 import org.keycloak.testsuite.arquillian.provider.OAuthClientProvider;
 import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider;
 import org.keycloak.testsuite.arquillian.provider.TestContextProvider;
@@ -43,6 +45,7 @@ import org.keycloak.testsuite.drone.HtmlUnitScreenshots;
 import org.keycloak.testsuite.drone.KeycloakDronePostSetup;
 import org.keycloak.testsuite.drone.KeycloakHtmlUnitInstantiator;
 import org.keycloak.testsuite.drone.KeycloakWebDriverConfigurator;
+import org.jboss.arquillian.test.spi.TestEnricher;
 
 /**
  *
@@ -57,12 +60,15 @@ public class KeycloakArquillianExtension implements LoadableExtension {
                 .service(ResourceProvider.class, SuiteContextProvider.class)
                 .service(ResourceProvider.class, TestContextProvider.class)
                 .service(ResourceProvider.class, AdminClientProvider.class)
-                .service(ResourceProvider.class, OAuthClientProvider.class);
+                .service(ResourceProvider.class, OAuthClientProvider.class)
+                .service(ResourceProvider.class, LoadBalancerControllerProvider.class);
 
         builder
                 .service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
                 .service(ApplicationArchiveProcessor.class, DeploymentArchiveProcessor.class)
                 .service(DeployableContainer.class, CustomKarafContainer.class)
+                .service(TestEnricher.class, CacheStatisticsControllerEnricher.class)
+                .observer(JmxConnectorRegistryCreator.class)
                 .observer(AuthServerTestEnricher.class)
                 .observer(AppServerTestEnricher.class)
                 .observer(H2TestEnricher.class);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
new file mode 100644
index 0000000..af1703d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
@@ -0,0 +1,61 @@
+package org.keycloak.testsuite.arquillian.provider;
+
+import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
+import java.lang.annotation.Annotation;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
+import org.keycloak.testsuite.arquillian.LoadBalancerController;
+import org.jboss.arquillian.container.spi.Container;
+import org.jboss.arquillian.container.spi.ContainerRegistry;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoadBalancerControllerProvider implements ResourceProvider {
+
+    @Inject
+    private Instance<ContainerRegistry> registry;
+
+    @Override
+    public boolean canProvide(Class<?> type) {
+        return type.equals(LoadBalancerController.class);
+    }
+
+    @Override
+    public Object lookup(ArquillianResource resource, Annotation... qualifiers) {
+        String balancerName = null;
+
+        // Check for the presence of possible qualifiers
+        for (Annotation a : qualifiers) {
+            Class<? extends Annotation> annotationType = a.annotationType();
+
+            if (annotationType.equals(LoadBalancer.class)) {
+                balancerName = ((LoadBalancer) a).value();
+            }
+        }
+
+        ContainerRegistry reg = registry.get();
+        Container container = null;
+        if (balancerName == null || "".equals(balancerName.trim())) {
+            if (reg.getContainers().size() == 1) {
+                container = reg.getContainers().get(0);
+            } else {
+                throw new IllegalArgumentException("Invalid load balancer configuration request - need to specify load balancer name in @LoadBalancerController");
+            }
+        } else {
+            container = reg.getContainer(balancerName);
+        }
+
+        if (container == null) {
+            throw new IllegalArgumentException("Invalid load balancer configuration - load balancer not found: '" + balancerName + "'");
+        }
+        if (! (container.getDeployableContainer() instanceof LoadBalancerController)) {
+            throw new IllegalArgumentException("Invalid load balancer configuration - container " + container.getName() + " is not a load balancer");
+        }
+
+        return container.getDeployableContainer();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
index fab303a..8a6d300 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
@@ -24,6 +24,7 @@ import java.util.Set;
 
 import org.keycloak.testsuite.arquillian.migration.MigrationContext;
 
+import java.util.LinkedList;
 import static org.keycloak.testsuite.util.MailServerConfiguration.FROM;
 import static org.keycloak.testsuite.util.MailServerConfiguration.HOST;
 import static org.keycloak.testsuite.util.MailServerConfiguration.PORT;
@@ -36,8 +37,8 @@ public final class SuiteContext {
 
     private final Set<ContainerInfo> container;
 
-    private ContainerInfo authServerInfo;
-    private final List<ContainerInfo> authServerBackendsInfo = new ArrayList<>();
+    private List<ContainerInfo> authServerInfo = new LinkedList<>();
+    private final List<List<ContainerInfo>> authServerBackendsInfo = new ArrayList<>();
 
     private ContainerInfo migratedAuthServerInfo;
     private final MigrationContext migrationContext = new MigrationContext();
@@ -72,17 +73,48 @@ public final class SuiteContext {
     }
 
     public ContainerInfo getAuthServerInfo() {
+        return getAuthServerInfo(0);
+    }
+
+    public ContainerInfo getAuthServerInfo(int dcIndex) {
+        return authServerInfo.get(dcIndex);
+    }
+
+    public List<ContainerInfo> getDcAuthServerInfo() {
         return authServerInfo;
     }
 
     public void setAuthServerInfo(ContainerInfo authServerInfo) {
-        this.authServerInfo = authServerInfo;
+        this.authServerInfo = new LinkedList<>();
+        this.authServerInfo.add(authServerInfo);
+    }
+
+    public void addAuthServerInfo(int dcIndex, ContainerInfo serverInfo) {
+        while (dcIndex >= authServerInfo.size()) {
+            authServerInfo.add(null);
+        }
+        this.authServerInfo.set(dcIndex, serverInfo);
     }
 
     public List<ContainerInfo> getAuthServerBackendsInfo() {
+        return getAuthServerBackendsInfo(0);
+    }
+
+    public List<ContainerInfo> getAuthServerBackendsInfo(int dcIndex) {
+        return authServerBackendsInfo.get(dcIndex);
+    }
+
+    public List<List<ContainerInfo>> getDcAuthServerBackendsInfo() {
         return authServerBackendsInfo;
     }
 
+    public void addAuthServerBackendsInfo(int dcIndex, ContainerInfo container) {
+        while (dcIndex >= authServerBackendsInfo.size()) {
+            authServerBackendsInfo.add(new LinkedList<>());
+        }
+        authServerBackendsInfo.get(dcIndex).add(container);
+    }
+
     public ContainerInfo getMigratedAuthServerInfo() {
         return migratedAuthServerInfo;
     }
@@ -96,7 +128,11 @@ public final class SuiteContext {
     }
 
     public boolean isAuthServerCluster() {
-        return !authServerBackendsInfo.isEmpty();
+        return ! authServerBackendsInfo.isEmpty();
+    }
+
+    public boolean isAuthServerCrossDc() {
+        return authServerBackendsInfo.size() > 1;
     }
 
     public boolean isAuthServerMigrationEnabled() {
@@ -113,19 +149,38 @@ public final class SuiteContext {
 
     @Override
     public String toString() {
-        String containers = "Auth server: " + (isAuthServerCluster() ? "\nFrontend: " : "")
-                + authServerInfo.getQualifier() + "\n";
-        for (ContainerInfo bInfo : getAuthServerBackendsInfo()) {
-            containers += "Backend: " + bInfo + "\n";
+        StringBuilder sb = new StringBuilder("SUITE CONTEXT:\nAuth server: ");
+
+        if (isAuthServerCrossDc()) {
+            for (int i = 0; i < authServerInfo.size(); i ++) {
+                ContainerInfo frontend = this.authServerInfo.get(i);
+                sb.append("\nFrontend (dc=").append(i).append("): ").append(frontend.getQualifier()).append("\n");
+            }
+
+            for (int i = 0; i < authServerBackendsInfo.size(); i ++) {
+                int dcIndex = i;
+                getDcAuthServerBackendsInfo().get(i).forEach(bInfo -> sb.append("Backend (dc=").append(dcIndex).append("): ").append(bInfo).append("\n"));
+            }
+        } else if (isAuthServerCluster()) {
+            sb.append(isAuthServerCluster() ? "\nFrontend: " : "")
+              .append(getAuthServerInfo().getQualifier())
+              .append("\n");
+
+            getAuthServerBackendsInfo().forEach(bInfo -> sb.append("  Backend: ").append(bInfo).append("\n"));
+        } else {
+          sb.append(getAuthServerInfo().getQualifier())
+            .append("\n");
         }
+
+
         if (isAuthServerMigrationEnabled()) {
-            containers += "Migrated from: " + System.getProperty("migrated.auth.server.version") + "\n";
+            sb.append("Migrated from: ").append(System.getProperty("migrated.auth.server.version")).append("\n");
         }
+
         if (isAdapterCompatTesting()) {
-            containers += "Adapter backward compatibility testing mode!\n";
+            sb.append("Adapter backward compatibility testing mode!\n");
         }
-        return "SUITE CONTEXT:\n"
-                + containers;
+        return sb.toString();
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
index 30b0405..cfe6d84 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.ws.rs.NotFoundException;
 
@@ -54,7 +55,7 @@ public final class TestContext {
     private boolean initialized;
 
     // Key is realmName, value are objects to clean after the test method
-    private Map<String, TestCleanup> cleanups = new HashMap<>();
+    private Map<String, TestCleanup> cleanups = new ConcurrentHashMap<>();
 
     public TestContext(SuiteContext suiteContext, Class testClass) {
         this.suiteContext = suiteContext;
@@ -146,7 +147,11 @@ public final class TestContext {
         TestCleanup cleanup = cleanups.get(realmName);
         if (cleanup == null) {
             cleanup = new TestCleanup(adminClient, realmName);
-            cleanups.put(realmName, cleanup);
+            TestCleanup existing = cleanups.putIfAbsent(realmName, cleanup);
+
+            if (existing != null) {
+                cleanup = existing;
+            }
         }
         return cleanup;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
new file mode 100644
index 0000000..29d512e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
@@ -0,0 +1,44 @@
+/*
+ * 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.pages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class LanguageComboboxAwarePage extends AbstractPage {
+
+    @FindBy(id = "kc-current-locale-link")
+    private WebElement languageText;
+
+    @FindBy(id = "kc-locale-dropdown")
+    private WebElement localeDropdown;
+
+    public String getLanguageDropdownText() {
+        return languageText.getText();
+    }
+
+    public void openLanguage(String language){
+        WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
+        String url = langLink.getAttribute("href");
+        driver.navigate().to(url);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
index 11d8fb2..b025ec7 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class LoginPage extends AbstractPage {
+public class LoginPage extends LanguageComboboxAwarePage {
 
     @ArquillianResource
     protected OAuthClient oauth;
@@ -75,12 +75,6 @@ public class LoginPage extends AbstractPage {
     private WebElement instruction;
 
 
-    @FindBy(id = "kc-current-locale-link")
-    private WebElement languageText;
-
-    @FindBy(id = "kc-locale-dropdown")
-    private WebElement localeDropdown;
-
     public void login(String username, String password) {
         usernameInput.clear();
         usernameInput.sendKeys(username);
@@ -191,14 +185,4 @@ public class LoginPage extends AbstractPage {
         assertCurrent();
     }
 
-    public String getLanguageDropdownText() {
-        return languageText.getText();
-    }
-
-    public void openLanguage(String language){
-        WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" +language +"']"));
-        String url = langLink.getAttribute("href");
-        driver.navigate().to(url);
-    }
-
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
index 93d203d..7a963e1 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class LoginPasswordUpdatePage extends AbstractPage {
+public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
 
     @FindBy(id = "password-new")
     private WebElement newPasswordInput;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
index 1a550ec..cfb1f06 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class OAuthGrantPage extends AbstractPage {
+public class OAuthGrantPage extends LanguageComboboxAwarePage {
 
     @FindBy(css = "input[name=\"accept\"]")
     private WebElement acceptButton;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 4c89eaa..207a317 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -102,7 +102,7 @@ public class OAuthClient {
 
     private String maxAge;
 
-    private String responseType = OAuth2Constants.CODE;
+    private String responseType;
 
     private String responseMode;
 
@@ -171,6 +171,8 @@ public class OAuthClient {
         clientSessionState = null;
         clientSessionHost = null;
         maxAge = null;
+        responseType = OAuth2Constants.CODE;
+        responseMode = null;
         nonce = null;
         request = null;
         requestUri = null;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
index 17ff44a..192712e 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
@@ -17,13 +17,13 @@
 
 package org.keycloak.testsuite.util;
 
-import java.util.LinkedList;
 import java.util.List;
 
 import javax.ws.rs.NotFoundException;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
 
 /**
  * Enlist resources to be cleaned after test method
@@ -32,18 +32,21 @@ import org.keycloak.admin.client.resource.RealmResource;
  */
 public class TestCleanup {
 
+    private static final String IDENTITY_PROVIDER_ALIASES = "IDENTITY_PROVIDER_ALIASES";
+    private static final String USER_IDS = "USER_IDS";
+    private static final String COMPONENT_IDS = "COMPONENT_IDS";
+    private static final String CLIENT_UUIDS = "CLIENT_UUIDS";
+    private static final String ROLE_IDS = "ROLE_IDS";
+    private static final String GROUP_IDS = "GROUP_IDS";
+    private static final String AUTH_FLOW_IDS = "AUTH_FLOW_IDS";
+    private static final String AUTH_CONFIG_IDS = "AUTH_CONFIG_IDS";
+
     private final Keycloak adminClient;
     private final String realmName;
 
+    // Key is kind of entity (eg. "client", "role", "user" etc), Values are all kind of entities of given type to cleanup
+    private ConcurrentMultivaluedHashMap<String, String> entities = new ConcurrentMultivaluedHashMap<>();
 
-    private List<String> identityProviderAliases;
-    private List<String> userIds;
-    private List<String> componentIds;
-    private List<String> clientUuids;
-    private List<String> roleIds;
-    private List<String> groupIds;
-    private List<String> authFlowIds;
-    private List<String> authConfigIds;
 
     public TestCleanup(Keycloak adminClient, String realmName) {
         this.adminClient = adminClient;
@@ -52,72 +55,49 @@ public class TestCleanup {
 
 
     public void addUserId(String userId) {
-        if (userIds == null) {
-            userIds = new LinkedList<>();
-        }
-        userIds.add(userId);
+        entities.add(USER_IDS, userId);
     }
 
 
     public void addIdentityProviderAlias(String identityProviderAlias) {
-        if (identityProviderAliases == null) {
-            identityProviderAliases = new LinkedList<>();
-        }
-        identityProviderAliases.add(identityProviderAlias);
+        entities.add(IDENTITY_PROVIDER_ALIASES, identityProviderAlias);
     }
 
 
     public void addComponentId(String componentId) {
-        if (componentIds == null) {
-            componentIds = new LinkedList<>();
-        }
-        componentIds.add(componentId);
+        entities.add(COMPONENT_IDS, componentId);
     }
 
 
     public void addClientUuid(String clientUuid) {
-        if (clientUuids == null) {
-            clientUuids = new LinkedList<>();
-        }
-        clientUuids.add(clientUuid);
+        entities.add(CLIENT_UUIDS, clientUuid);
     }
 
 
     public void addRoleId(String roleId) {
-        if (roleIds == null) {
-            roleIds = new LinkedList<>();
-        }
-        roleIds.add(roleId);
+        entities.add(ROLE_IDS, roleId);
     }
 
 
     public void addGroupId(String groupId) {
-        if (groupIds == null) {
-            groupIds = new LinkedList<>();
-        }
-        groupIds.add(groupId);
+        entities.add(GROUP_IDS, groupId);
     }
 
 
     public void addAuthenticationFlowId(String flowId) {
-        if (authFlowIds == null) {
-            authFlowIds = new LinkedList<>();
-        }
-        authFlowIds.add(flowId);
+        entities.add(AUTH_FLOW_IDS, flowId);
     }
 
 
     public void addAuthenticationConfigId(String executionConfigId) {
-        if (authConfigIds == null) {
-            authConfigIds = new LinkedList<>();
-        }
-        authConfigIds.add(executionConfigId);
+        entities.add(AUTH_CONFIG_IDS, executionConfigId);
     }
 
 
     public void executeCleanup() {
         RealmResource realm = adminClient.realm(realmName);
 
+        List<String> userIds = entities.get(USER_IDS);
         if (userIds != null) {
             for (String userId : userIds) {
                 try {
@@ -128,6 +108,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> identityProviderAliases = entities.get(IDENTITY_PROVIDER_ALIASES);
         if (identityProviderAliases != null) {
             for (String idpAlias : identityProviderAliases) {
                 try {
@@ -138,6 +119,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> componentIds = entities.get(COMPONENT_IDS);
         if (componentIds != null) {
             for (String componentId : componentIds) {
                 try {
@@ -148,6 +130,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> clientUuids = entities.get(CLIENT_UUIDS);
         if (clientUuids != null) {
             for (String clientUuId : clientUuids) {
                 try {
@@ -158,6 +141,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> roleIds = entities.get(ROLE_IDS);
         if (roleIds != null) {
             for (String roleId : roleIds) {
                 try {
@@ -168,6 +152,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> groupIds = entities.get(GROUP_IDS);
         if (groupIds != null) {
             for (String groupId : groupIds) {
                 try {
@@ -178,6 +163,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> authFlowIds = entities.get(AUTH_FLOW_IDS);
         if (authFlowIds != null) {
             for (String flowId : authFlowIds) {
                 try {
@@ -188,6 +174,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> authConfigIds = entities.get(AUTH_CONFIG_IDS);
         if (authConfigIds != null) {
             for (String configId : authConfigIds) {
                 try {
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 ecf48bf..420fad8 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
@@ -198,6 +198,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
     @Page
     private SAMLIDPInitiatedLogin samlidpInitiatedLoginPage;
 
+    @Page
+    protected SalesPostAutodetectServlet salesPostAutodetectServletPage;
+
     public static final String FORBIDDEN_TEXT = "HTTP status code: 403";
 
     @Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME)
@@ -325,6 +328,11 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
         return samlServletDeployment(EmployeeServlet.DEPLOYMENT_NAME, "employee/WEB-INF/web.xml", SamlSPFacade.class, ServletTestUtils.class);
     }
 
+    @Deployment(name = SalesPostAutodetectServlet.DEPLOYMENT_NAME)
+    protected static WebArchive salesPostAutodetect() {
+        return samlServletDeployment(SalesPostAutodetectServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
+    }
+
     @Override
     public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
         testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
@@ -1115,6 +1123,54 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
         checkLoggedOut(differentCookieNameServletPage, testRealmSAMLPostLoginPage);
     }
 
+    @Test
+    /* KEYCLOAK-4980 */
+    public void testAutodetectBearerOnly() throws Exception {
+        Client client = ClientBuilder.newClient();
+
+        // Do not redirect client to login page if it's an XHR
+        WebTarget target = client.target(salesPostAutodetectServletPage.toString());
+        Response response = target.request().header("X-Requested-With", "XMLHttpRequest").get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        // Do not redirect client to login page if it's a partial Faces request
+        response = target.request().header("Faces-Request", "partial/ajax").get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        // Do not redirect client to login page if it's a SOAP request
+        response = target.request().header("SOAPAction", "").get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        // Do not redirect client to login page if Accept header is missing
+        response = target.request().get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        // Do not redirect client to login page if client does not understand HTML reponses
+        response = target.request().header(HttpHeaders.ACCEPT, "application/json,text/xml").get();
+        Assert.assertEquals(401, response.getStatus());
+        response.close();
+
+        // Redirect client to login page if it's not an XHR
+        response = target.request().header("X-Requested-With", "Dont-Know").header(HttpHeaders.ACCEPT, "*/*").get();
+        Assert.assertEquals(200, response.getStatus());
+        response.close();
+
+        // Redirect client to login page if client explicitely understands HTML responses
+        response = target.request().header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9").get();
+        Assert.assertEquals(200, response.getStatus());
+        response.close();
+
+        // Redirect client to login page if client understands all response types
+        response = target.request().header(HttpHeaders.ACCEPT, "*/*").get();
+        Assert.assertEquals(200, response.getStatus());
+        response.close();
+        client.close();
+    }
+
     private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
         return RealmsResource
                 .protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
index 244323b..ae122f6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
@@ -107,7 +107,9 @@ public abstract class AbstractClientTest extends AbstractAuthTest {
         clientRep.setPublicClient(Boolean.FALSE);
         clientRep.setAuthorizationServicesEnabled(Boolean.TRUE);
         clientRep.setServiceAccountsEnabled(Boolean.TRUE);
-        return createClient(clientRep);
+        String id = createClient(clientRep);
+        assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientResourcePath(id), ResourceType.AUTHORIZATION_RESOURCE_SERVER);
+        return id;
     }
 
     protected ClientRepresentation createOidcClientRep(String name) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ExportAuthorizationSettingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ExportAuthorizationSettingsTest.java
index 6f5e65e..c71d12f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ExportAuthorizationSettingsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ExportAuthorizationSettingsTest.java
@@ -22,7 +22,6 @@ import java.util.List;
 import java.util.Map;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.AuthorizationResource;
 import org.keycloak.admin.client.resource.ClientResource;
@@ -120,7 +119,6 @@ public class ExportAuthorizationSettingsTest extends AbstractAuthorizationTest {
     
     //KEYCLOAK-4983
     @Test
-    @Ignore
     public void testRoleBasedPolicyWithMultipleRoles() {
         ClientResource clientResource = getClientResource();
 
@@ -152,31 +150,17 @@ public class ExportAuthorizationSettingsTest extends AbstractAuthorizationTest {
         //export authorization settings
         ResourceServerRepresentation exportSettings = authorizationResource.exportSettings();
         
-        //delete test-resource-server client
-        testRealmResource().clients().get(clientResource.toRepresentation().getId()).remove();
-        
-        //clear cache
-        testRealmResource().clearRealmCache();
-        //workaround for the fact that clearing realm cache doesn't clear authz cache
-        testingClient.testing("test").cache("authorization").clear();
-        
-        //create new client
-        ClientRepresentation client = ClientBuilder.create()
-                .clientId(RESOURCE_SERVER_CLIENT_ID)
-                .authorizationServicesEnabled(true)
-                .serviceAccountsEnabled(true)
-                .build();
-        testRealmResource().clients().create(client).close();
-        
-        //import exported settings
-        AuthorizationResource authorization = testRealmResource().clients().get(getClientByClientId(RESOURCE_SERVER_CLIENT_ID).getId()).authorization();
-        authorization.importSettings(exportSettings);
-        
-        //check imported settings - TODO
-        PolicyRepresentation result = authorization.policies().findByName("role-based-policy");
-        Map<String, String> config1 = result.getConfig();
-        ResourceServerRepresentation settings = authorization.getSettings();
-        System.out.println("");
+        boolean found = false;
+        for (PolicyRepresentation p : exportSettings.getPolicies()) {
+            if (p.getName().equals("role-based-policy")) {
+                found = true;
+                Assert.assertTrue(p.getConfig().get("roles").contains("test-client-1/client-role") && 
+                        p.getConfig().get("roles").contains("test-client-2/client-role"));
+            }
+        }
+        if (!found) {
+            Assert.fail("Policy \"role-based-policy\" was not found in exported settings.");
+        }
     }
     
     private ClientRepresentation getClientByClientId(String clientId) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..57d86a7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2017 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.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.GroupPoliciesResource;
+import org.keycloak.admin.client.resource.GroupPolicyResource;
+import org.keycloak.admin.client.resource.PolicyResource;
+import org.keycloak.admin.client.resource.RolePoliciesResource;
+import org.keycloak.admin.client.resource.RolePolicyResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
+
+    @Override
+    protected RealmBuilder createTestRealm() {
+        return super.createTestRealm().group(GroupBuilder.create().name("Group A")
+                .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                    if ("Group B".equals(name)) {
+                        return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                            @Override
+                            public GroupRepresentation apply(String name) {
+                                return GroupBuilder.create().name(name).build();
+                            }
+                        }).collect(Collectors.toList())).build();
+                    }
+                    return GroupBuilder.create().name(name).build();
+                }).collect(Collectors.toList()))
+                .build()).group(GroupBuilder.create().name("Group E").build());
+    }
+
+    @Test
+    public void testCreate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+    }
+
+    @Test
+    public void testUpdate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Update Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+
+        representation.setName("changed");
+        representation.setDescription("changed");
+        representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+        representation.setLogic(Logic.POSITIVE);
+        representation.removeGroup("/Group A/Group B");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        GroupPolicyResource permission = policies.findById(representation.getId());
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) {
+            if (roleDefinition.getPath().equals("Group E")) {
+                roleDefinition.setExtendChildren(true);
+            }
+        }
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        representation.getGroups().clear();
+        representation.addGroupPath("/Group A/Group B");
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+    }
+
+    @Test
+    public void testDelete() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Delete Group Policy");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        policies.findById(created.getId()).remove();
+
+        GroupPolicyResource removed = policies.findById(created.getId());
+
+        try {
+            removed.toRepresentation();
+            fail("Permission not removed");
+        } catch (NotFoundException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testGenericConfig() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Test Generic Config Permission");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        PolicyResource policy = authorization.policies().policy(created.getId());
+        PolicyRepresentation genericConfig = policy.toRepresentation();
+
+        assertNotNull(genericConfig.getConfig());
+        assertNotNull(genericConfig.getConfig().get("groups"));
+
+        GroupRepresentation group = getRealm().groups().groups().stream().filter(groupRepresentation -> groupRepresentation.getName().equals("Group A")).findFirst().get();
+
+        assertTrue(genericConfig.getConfig().get("groups").contains(group.getId()));
+    }
+
+    private void assertCreated(AuthorizationResource authorization, GroupPolicyRepresentation representation) {
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+        GroupPolicyResource policy = policies.findById(created.getId());
+        assertRepresentation(representation, policy);
+    }
+
+    private void assertRepresentation(GroupPolicyRepresentation representation, GroupPolicyResource permission) {
+        GroupPolicyRepresentation actual = permission.toRepresentation();
+        assertRepresentation(representation, actual, () -> permission.resources(), () -> Collections.emptyList(), () -> permission.associatedPolicies());
+        assertEquals(representation.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !representation.getGroups().stream()
+                .filter(groupDefinition -> getGroupPath(actualDefinition.getId()).equals(getCanonicalGroupPath(groupDefinition.getPath())) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+    }
+
+    private String getGroupPath(String id) {
+        return getRealm().groups().group(id).toRepresentation().getPath();
+    }
+
+    private String getCanonicalGroupPath(String path) {
+        if (path.charAt(0) == '/') {
+            return path;
+        }
+        return "/" + path;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/InitialAccessTokenResourceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/InitialAccessTokenResourceTest.java
index 055bfa2..064e7b8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/InitialAccessTokenResourceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/InitialAccessTokenResourceTest.java
@@ -20,14 +20,17 @@ package org.keycloak.testsuite.admin;
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.ClientInitialAccessResource;
+import org.keycloak.client.registration.ClientRegistrationException;
 import org.keycloak.common.util.Time;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
 import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
 import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.util.AdminEventPaths;
 
 import java.util.List;
+import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -88,4 +91,40 @@ public class InitialAccessTokenResourceTest extends AbstractAdminTest {
         assertEquals(5, list.get(0).getCount() + list.get(1).getCount());
     }
 
+
+    @Test
+    public void testPeriodicExpiration() throws ClientRegistrationException, InterruptedException {
+        ClientInitialAccessPresentation response1 = resource.create(new ClientInitialAccessCreatePresentation(1, 1));
+        ClientInitialAccessPresentation response2 = resource.create(new ClientInitialAccessCreatePresentation(1000, 1));
+        ClientInitialAccessPresentation response3 = resource.create(new ClientInitialAccessCreatePresentation(1000, 0));
+        ClientInitialAccessPresentation response4 = resource.create(new ClientInitialAccessCreatePresentation(0, 1));
+
+        List<ClientInitialAccessPresentation> list = resource.list();
+        assertEquals(4, list.size());
+
+        setTimeOffset(10);
+
+        testingClient.testing().removeExpired(REALM_NAME);
+
+        list = resource.list();
+        assertEquals(2, list.size());
+
+        List<String> remainingIds = list.stream()
+                .map(initialAccessPresentation -> initialAccessPresentation.getId())
+                .collect(Collectors.toList());
+
+        Assert.assertNames(remainingIds, response2.getId(), response4.getId());
+
+        setTimeOffset(2000);
+
+        testingClient.testing().removeExpired(REALM_NAME);
+
+        list = resource.list();
+        assertEquals(1, list.size());
+        Assert.assertEquals(list.get(0).getId(), response4.getId());
+
+        // Cleanup
+        realm.clientInitialAccess().delete(response4.getId());
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
index 15f0564..d7f494f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
@@ -156,6 +156,65 @@ public class UserStorageRestTest extends AbstractAdminTest {
 
     }
 
+
+    // KEYCLOAK-4438
+    @Test
+    public void testKerberosAuthenticatorDisabledWhenProviderRemoved() {
+        // Assert kerberos authenticator DISABLED
+        AuthenticationExecutionInfoRepresentation kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+        // create LDAP provider with kerberos
+        ComponentRepresentation ldapRep = new ComponentRepresentation();
+        ldapRep.setName("ldap2");
+        ldapRep.setProviderId("ldap");
+        ldapRep.setProviderType(UserStorageProvider.class.getName());
+        ldapRep.setConfig(new MultivaluedHashMap<>());
+        ldapRep.getConfig().putSingle("priority", Integer.toString(2));
+        ldapRep.getConfig().putSingle(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "true");
+
+
+        String id = createComponent(ldapRep);
+
+        // Assert kerberos authenticator ALTERNATIVE
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+        // Remove LDAP provider
+        realm.components().component(id).remove();
+
+        // Assert kerberos authenticator DISABLED
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+        // Add kerberos provider
+        ComponentRepresentation kerberosRep = new ComponentRepresentation();
+        kerberosRep.setName("kerberos");
+        kerberosRep.setProviderId("kerberos");
+        kerberosRep.setProviderType(UserStorageProvider.class.getName());
+        kerberosRep.setConfig(new MultivaluedHashMap<>());
+        kerberosRep.getConfig().putSingle("priority", Integer.toString(2));
+
+        id = createComponent(kerberosRep);
+
+
+        // Assert kerberos authenticator ALTERNATIVE
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+        // Switch kerberos authenticator to REQUIRED
+        kerberosExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString());
+        realm.flows().updateExecutions("browser", kerberosExecution);
+
+        // Remove Kerberos provider
+        realm.components().component(id).remove();
+
+        // Assert kerberos authenticator DISABLED
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+    }
+
+
     @Test
     public void testValidateAndCreateLdapProvider() {
         // Invalid filter
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
new file mode 100644
index 0000000..cc4b911
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2017 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.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupNamePolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+        createResource("Resource C");
+
+        createGroupPolicy("Only Group A Policy", "/Group A", true);
+        createGroupPolicy("Only Group B Policy", "/Group A/Group B", false);
+        createGroupPolicy("Only Group C Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Only Group A Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Group B Policy");
+        createResourcePermission("Resource C Permission", "Resource C", "Only Group C Policy");
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        group = getGroup("/Group A/Group B");
+        user = realm.users().search("alice").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+    }
+
+    @Test
+    public void testExactNameMatch() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        try {
+            authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        AuthorizationResponse response = authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        request = new PermissionRequest();
+
+        request.setResourceSetName("Resource C");
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
new file mode 100644
index 0000000..19f74b4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 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.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPathPolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        config.put("full.path", "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+
+        createGroupPolicy("Parent And Children Policy", "/Group A", true);
+        createGroupPolicy("Only Children Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Parent And Children Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Children Policy");
+    }
+
+    @Test
+    public void testAllowParentAndChildren() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        AuthorizationResponse response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java
new file mode 100644
index 0000000..5f9fd0e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java
@@ -0,0 +1,83 @@
+package org.keycloak.testsuite.broker;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.arquillian.SuiteContext;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
+
+public class KcSamlSignedDocumentOnlyBrokerTest extends KcSamlBrokerTest {
+
+    public static class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration {
+
+        @Override
+        public RealmRepresentation createProviderRealm() {
+            RealmRepresentation realm = super.createProviderRealm();
+
+            realm.setPublicKey(REALM_PUBLIC_KEY);
+            realm.setPrivateKey(REALM_PRIVATE_KEY);
+
+            return realm;
+        }
+
+        @Override
+        public RealmRepresentation createConsumerRealm() {
+            RealmRepresentation realm = super.createConsumerRealm();
+
+            realm.setPublicKey(REALM_PUBLIC_KEY);
+            realm.setPrivateKey(REALM_PRIVATE_KEY);
+
+            return realm;
+        }
+
+        @Override
+        public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
+            List<ClientRepresentation> clientRepresentationList = super.createProviderClients(suiteContext);
+
+            for (ClientRepresentation client : clientRepresentationList) {
+                client.setClientAuthenticatorType("client-secret");
+                client.setSurrogateAuthRequired(false);
+
+                Map<String, String> attributes = client.getAttributes();
+                if (attributes == null) {
+                    attributes = new HashMap<>();
+                    client.setAttributes(attributes);
+                }
+
+                attributes.put("saml.assertion.signature", "false");
+                attributes.put("saml.server.signature", "true");
+                attributes.put("saml.client.signature", "true");
+                attributes.put("saml.signature.algorithm", "RSA_SHA256");
+                attributes.put("saml.signing.private.key", IDP_SAML_SIGN_KEY);
+                attributes.put("saml.signing.certificate", IDP_SAML_SIGN_CERT);
+            }
+
+            return clientRepresentationList;
+        }
+
+        @Override
+        public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
+            IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext);
+
+            Map<String, String> config = result.getConfig();
+
+            config.put("validateSignature", "true");
+            config.put("wantAssertionsSigned", "false");
+            config.put("wantAuthnRequestsSigned", "true");
+            config.put("signingCertificate", IDP_SAML_SIGN_CERT);
+
+            return result;
+        }
+    }
+
+    @Override
+    protected BrokerConfiguration getBrokerConfiguration() {
+        return KcSamlSignedBrokerConfiguration.INSTANCE;
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
index c5bb785..8666e04 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java
@@ -18,6 +18,7 @@
 package org.keycloak.testsuite.client;
 
 
+import org.apache.commons.lang.StringUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.ClientResource;
@@ -44,6 +45,7 @@ import org.keycloak.testsuite.util.UserInfoClientUtil;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.core.Response;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.Collections;
 import java.util.List;
 
@@ -319,11 +321,20 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
         oauth.openLoginForm();
         loginResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
         accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
+
+        // Assert token payloads don't contain more than one "sub"
+        String accessTokenPayload = getPayload(accessTokenResponse.getAccessToken());
+        Assert.assertEquals(1, StringUtils.countMatches(accessTokenPayload, "\"sub\""));
+        String idTokenPayload = getPayload(accessTokenResponse.getIdToken());
+        Assert.assertEquals(1, StringUtils.countMatches(idTokenPayload, "\"sub\""));
+        String refreshTokenPayload = getPayload(accessTokenResponse.getRefreshToken());
+        Assert.assertEquals(1, StringUtils.countMatches(refreshTokenPayload, "\"sub\""));
+
         accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
         Assert.assertEquals("test-user", accessToken.getPreferredUsername());
         Assert.assertEquals("test-user@localhost", accessToken.getEmail());
 
-        // Assert pairwise client has different subject like userId
+        // Assert pairwise client has different subject than userId
         String pairwiseUserId = accessToken.getSubject();
         Assert.assertNotEquals(pairwiseUserId, user.getId());
 
@@ -339,4 +350,9 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
             jaxrsClient.close();
         }
     }
-}
+
+    private String getPayload(String token) {
+        String payloadBase64 = token.split("\\.")[1];
+        return new String(Base64.getDecoder().decode(payloadBase64));
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
new file mode 100644
index 0000000..2baa336
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 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.crossdc;
+
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.events.EventsListenerProviderFactory;
+import org.keycloak.testsuite.util.TestCleanup;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import static org.junit.Assert.assertThat;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest {
+
+    protected static final String REALM_NAME = "admin-client-test";
+
+    protected RealmResource realm;
+    protected String realmId;
+
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+        findTestApp(testRealm).setDirectAccessGrantsEnabled(true);
+    }
+
+
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        super.addTestRealms(testRealms);
+
+        RealmRepresentation adminRealmRep = new RealmRepresentation();
+        adminRealmRep.setId(REALM_NAME);
+        adminRealmRep.setRealm(REALM_NAME);
+        adminRealmRep.setEnabled(true);
+        Map<String, String> config = new HashMap<>();
+        config.put("from", "auto@keycloak.org");
+        config.put("host", "localhost");
+        config.put("port", "3025");
+        adminRealmRep.setSmtpServer(config);
+
+        List<String> eventListeners = new ArrayList<>();
+        eventListeners.add(JBossLoggingEventListenerProviderFactory.ID);
+        eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID);
+        adminRealmRep.setEventsListeners(eventListeners);
+
+        testRealms.add(adminRealmRep);
+    }
+
+    @Before
+    public void setRealm() {
+        realm = adminClient.realm(REALM_NAME);
+        realmId = realm.toRepresentation().getId();
+    }
+
+    @Override
+    protected TestCleanup getCleanup() {
+        return getCleanup(REALM_NAME);
+    }
+
+    protected <T extends Comparable> void assertSingleStatistics(InfinispanStatistics stats, String key, Runnable testedCode, Function<T, Matcher<? super T>> matcherOnOldStat) {
+        stats.reset();
+
+        T oldStat = (T) stats.getSingleStatistics(key);
+        testedCode.run();
+
+        Retry.execute(() -> {
+            T newStat = (T) stats.getSingleStatistics(key);
+
+            Matcher<? super T> matcherInstance = matcherOnOldStat.apply(oldStat);
+            assertThat(newStat, matcherInstance);
+        }, 5, 200);
+    }
+
+    protected void assertStatistics(InfinispanStatistics stats, Runnable testedCode, BiConsumer<Map<String, Object>, Map<String, Object>> assertionOnStats) {
+        stats.reset();
+
+        Map<String, Object> oldStat = stats.getStatistics();
+        testedCode.run();
+
+        Retry.execute(() -> {
+            Map<String, Object> newStat = stats.getStatistics();
+            assertionOnStats.accept(oldStat, newStat);
+        }, 5, 200);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
new file mode 100644
index 0000000..c88c0c1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 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.crossdc;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.models.Constants;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
+import org.keycloak.testsuite.arquillian.LoadBalancerController;
+import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
+import org.keycloak.testsuite.auth.page.AuthRealm;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.After;
+import org.junit.Before;
+
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest {
+
+    // Keep the following constants in sync with arquillian
+    public static final String QUALIFIER_NODE_BALANCER = "auth-server-balancer-cross-dc";
+
+    @ArquillianResource
+    @LoadBalancer(value = QUALIFIER_NODE_BALANCER)
+    protected LoadBalancerController loadBalancerCtrl;
+
+    @ArquillianResource
+    protected ContainerController containerController;
+
+    protected Map<ContainerInfo, Keycloak> backendAdminClients = new HashMap<>();
+
+    @After
+    @Before
+    public void enableOnlyFirstNodeInFirstDc() {
+        this.loadBalancerCtrl.disableAllBackendNodes();
+        loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(0)
+          .findFirst()
+          .orElseThrow(() -> new IllegalStateException("No node is started automatically"))
+          .getQualifier()
+        );
+    }
+
+    @Before
+    public void terminateManuallyStartedServers() {
+        log.debug("Halting all nodes that are started manually");
+        this.suiteContext.getDcAuthServerBackendsInfo().stream()
+          .flatMap(List::stream)
+          .filter(ContainerInfo::isStarted)
+          .filter(ContainerInfo::isManual)
+          .map(ContainerInfo::getQualifier)
+          .forEach(containerController::stop);
+    }
+
+    @Override
+    public void importTestRealms() {
+        enableOnlyFirstNodeInFirstDc();
+        super.importTestRealms();
+    }
+
+    @Override
+    public void afterAbstractKeycloakTest() {
+        enableOnlyFirstNodeInFirstDc();
+        super.afterAbstractKeycloakTest();
+    }
+
+    @Override
+    public void deleteCookies() {
+        enableOnlyFirstNodeInFirstDc();
+        super.deleteCookies();
+    }
+
+    @Before
+    public void initLoadBalancer() {
+        log.debug("Initializing load balancer - only enabling started nodes in the first DC");
+        this.loadBalancerCtrl.disableAllBackendNodes();
+        // Enable only the started nodes in each datacenter
+        this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream()
+          .filter(ContainerInfo::isStarted)
+          .map(ContainerInfo::getQualifier)
+          .forEach(loadBalancerCtrl::enableBackendNodeByName);
+    }
+
+    protected Keycloak createAdminClientFor(ContainerInfo node) {
+        log.info("Initializing admin client for " + node.getContextRoot() + "/auth");
+        return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
+    }
+
+    /**
+     * Creates admin client directed to the given node.
+     * @param node
+     * @return
+     */
+    protected Keycloak getAdminClientFor(ContainerInfo node) {
+        Keycloak adminClient = backendAdminClients.get(node);
+        if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) {
+            adminClient = this.adminClient;
+        }
+        return adminClient;
+    }
+
+    /**
+     * Disables routing requests to the given data center in the load balancer.
+     * @param dcIndex
+     */
+    public void disableDcOnLoadBalancer(int dcIndex) {
+        log.infof("Disabling load balancer for dc=%d", dcIndex);
+        this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
+    }
+
+    /**
+     * Enables routing requests to all started nodes to the given data center in the load balancer.
+     * @param dcIndex
+     */
+    public void enableDcOnLoadBalancer(int dcIndex) {
+        log.infof("Enabling load balancer for dc=%d", dcIndex);
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        if (! dcNodes.stream().anyMatch(ContainerInfo::isStarted)) {
+            log.warnf("No node is started in DC %d", dcIndex);
+        } else {
+            dcNodes.stream()
+              .filter(ContainerInfo::isStarted)
+              .forEach(c -> loadBalancerCtrl.enableBackendNodeByName(c.getQualifier()));
+        }
+    }
+
+    /**
+     * Disables routing requests to the given node within the given data center in the load balancer.
+     * @param dcIndex
+     * @param nodeIndex
+     */
+    public void disableLoadBalancerNode(int dcIndex, int nodeIndex) {
+        log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
+        loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier());
+    }
+
+    /**
+     * Enables routing requests to the given node within the given data center in the load balancer.
+     * @param dcIndex
+     * @param nodeIndex
+     */
+    public void enableLoadBalancerNode(int dcIndex, int nodeIndex) {
+        log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
+        final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex);
+        if (backendNode == null) {
+            throw new IllegalArgumentException("Invalid node with index " + nodeIndex + " for DC " + dcIndex);
+        }
+        if (! backendNode.isStarted()) {
+            log.warnf("Node %s is not started in DC %d", backendNode.getQualifier(), dcIndex);
+        }
+        loadBalancerCtrl.enableBackendNodeByName(backendNode.getQualifier());
+    }
+
+    /**
+     * Starts a manually-controlled backend auth-server node in cross-DC scenario.
+     * @param dcIndex
+     * @param nodeIndex
+     * @return Started instance descriptor.
+     */
+    public ContainerInfo startBackendNode(int dcIndex, int nodeIndex) {
+        assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
+        ContainerInfo dcNode = dcNodes.get(nodeIndex);
+        assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
+        containerController.start(dcNode.getQualifier());
+        return dcNode;
+    }
+
+    /**
+     * Stops a manually-controlled backend auth-server node in cross-DC scenario.
+     * @param dcIndex
+     * @param nodeIndex
+     * @return Stopped instance descriptor.
+     */
+    public ContainerInfo stopBackendNode(int dcIndex, int nodeIndex) {
+        assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
+        ContainerInfo dcNode = dcNodes.get(nodeIndex);
+        assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
+        containerController.stop(dcNode.getQualifier());
+        return dcNode;
+    }
+
+    /**
+     * Returns stream of all nodes in the given dc that are started manually.
+     * @param dcIndex
+     * @return
+     */
+    public Stream<ContainerInfo> getManuallyStartedBackendNodes(int dcIndex) {
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        return dcNodes.stream().filter(ContainerInfo::isManual);
+    }
+
+    /**
+     * Returns stream of all nodes in the given dc that are started automatically.
+     * @param dcIndex
+     * @return
+     */
+    public Stream<ContainerInfo> getAutomaticallyStartedBackendNodes(int dcIndex) {
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        return dcNodes.stream().filter(c -> ! c.isManual());
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
new file mode 100644
index 0000000..dbef2fc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2017 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.crossdc;
+
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.MailUtils;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import javax.ws.rs.core.Response;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.util.concurrent.TimeUnit;
+import org.hamcrest.Matchers;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
+
+    @Rule
+    public GreenMailRule greenMail = new GreenMailRule();
+
+    @Page
+    protected LoginPasswordUpdatePage passwordUpdatePage;
+
+    @Page
+    protected ErrorPage errorPage;
+
+    private String createUser(UserRepresentation userRep) {
+        Response response = realm.users().create(userRep);
+        String createdId = ApiUtil.getCreatedId(response);
+        response.close();
+
+        getCleanup().addUserId(createdId);
+
+        return createdId;
+    }
+
+    @Test
+    public void sendResetPasswordEmailSuccessWorksInCrossDc(
+      @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
+      @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
+      @JmxInfinispanCacheStatistics(dcIndex=1, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics,
+      @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+        startBackendNode(0, 1);
+        cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS);
+
+        Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+
+        UserRepresentation userRep = new UserRepresentation();
+        userRep.setEnabled(true);
+        userRep.setUsername("user1");
+        userRep.setEmail("user1@test.com");
+
+        String id = createUser(userRep);
+
+        UserResource user = realm.users().get(id);
+        List<String> actions = new LinkedList<>();
+        actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
+        user.executeActionsEmail(actions);
+
+        Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+
+        String link = MailUtils.getPasswordResetEmailLink(message);
+
+        Retry.execute(() -> channelStatisticsCrossDc.reset(), 3, 100);
+
+        assertSingleStatistics(cacheDc0Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES,
+          () -> driver.navigate().to(link),
+          Matchers::is
+        );
+
+        passwordUpdatePage.assertCurrent();
+
+        // Verify that there was at least one message sent via the channel
+        assertSingleStatistics(channelStatisticsCrossDc, Constants.STAT_CHANNEL_SENT_MESSAGES,
+          () -> passwordUpdatePage.changePassword("new-pass", "new-pass"),
+          old -> greaterThan((Comparable) 0l)
+        );
+
+        // Verify that the caches are synchronized
+        assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries));
+        assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES),
+                is(cacheDc1Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES)));
+
+        assertEquals("Your account has been updated.", driver.getTitle());
+
+        disableDcOnLoadBalancer(0);
+        enableDcOnLoadBalancer(1);
+
+        driver.navigate().to(link);
+        errorPage.assertCurrent();
+    }
+
+    @Ignore("KEYCLOAK-5030")
+    @Test
+    public void sendResetPasswordEmailAfterNewNodeAdded() throws IOException, MessagingException {
+        disableDcOnLoadBalancer(1);
+
+        UserRepresentation userRep = new UserRepresentation();
+        userRep.setEnabled(true);
+        userRep.setUsername("user1");
+        userRep.setEmail("user1@test.com");
+
+        String id = createUser(userRep);
+
+        UserResource user = realm.users().get(id);
+        List<String> actions = new LinkedList<>();
+        actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
+        user.executeActionsEmail(actions);
+
+        Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+
+        String link = MailUtils.getPasswordResetEmailLink(message);
+
+        driver.navigate().to(link);
+
+        passwordUpdatePage.assertCurrent();
+
+        passwordUpdatePage.changePassword("new-pass", "new-pass");
+
+        assertEquals("Your account has been updated.", driver.getTitle());
+
+        disableDcOnLoadBalancer(0);
+        getManuallyStartedBackendNodes(1)
+          .findFirst()
+          .ifPresent(c -> {
+              containerController.start(c.getQualifier());
+              loadBalancerCtrl.enableBackendNodeByName(c.getQualifier());
+          });
+
+        Retry.execute(() -> {
+            driver.navigate().to(link);
+            errorPage.assertCurrent();
+        }, 3, 400);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index b29abc1..677430d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -627,12 +627,13 @@ public class ExportImportUtil {
         assertPredicate(scopes, scopePredicates);
 
         List<PolicyRepresentation> policies = authzResource.policies().policies();
-        Assert.assertEquals(13, policies.size());
+        Assert.assertEquals(14, policies.size());
         List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
         policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "Any User Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(representation -> "Client and Realm Role Policy".equals(representation.getName()));
         policyPredicates.add(representation -> "Client Test Policy".equals(representation.getName()));
+        policyPredicates.add(representation -> "Group Policy Test".equals(representation.getName()));
         policyPredicates.add(policyRepresentation -> "Only Premium User Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 5c9ff74..2d92719 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -16,19 +16,30 @@
  */
 package org.keycloak.testsuite.i18n;
 
+import java.util.Arrays;
+
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
 import org.junit.Assert;
 import org.junit.Test;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.adapters.HttpClientBuilder;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
 import org.keycloak.testsuite.pages.LoginPage;
 
 import javax.ws.rs.core.Response;
 import org.jboss.arquillian.graphene.page.Page;
 import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.util.IdentityProviderBuilder;
 
 /**
@@ -38,8 +49,18 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder;
 public class LoginPageTest extends AbstractI18NTest {
 
     @Page
+    protected AppPage appPage;
+
+    @Page
     protected LoginPage loginPage;
 
+    @Page
+    protected LoginPasswordUpdatePage changePasswordPage;
+
+    @Page
+    protected OAuthGrantPage grantPage;
+
+
     @Override
     public void configureTestRealm(RealmRepresentation testRealm) {
         testRealm.addIdentityProvider(IdentityProviderBuilder.create()
@@ -63,11 +84,7 @@ public class LoginPageTest extends AbstractI18NTest {
         loginPage.open();
         Assert.assertEquals("English", loginPage.getLanguageDropdownText());
 
-        loginPage.openLanguage("Deutsch");
-        Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
-
-        loginPage.openLanguage("English");
-        Assert.assertEquals("English", loginPage.getLanguageDropdownText());
+        switchLanguageToGermanAndBack("Username or email", "Benutzername oder E-Mail", loginPage);
     }
 
     @Test
@@ -109,6 +126,8 @@ public class LoginPageTest extends AbstractI18NTest {
 
         response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
         Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
+
+        client.close();
     }
 
     @Test
@@ -119,4 +138,73 @@ public class LoginPageTest extends AbstractI18NTest {
         Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
 
     }
+
+
+    // KEYCLOAK-3887
+    @Test
+    public void languageChangeRequiredActions() {
+        UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
+        UserRepresentation userRep = user.toRepresentation();
+        userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));
+        user.update(userRep);
+
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+        changePasswordPage.assertCurrent();
+        Assert.assertEquals("English", changePasswordPage.getLanguageDropdownText());
+
+        // Switch language
+        switchLanguageToGermanAndBack("Update password", "Passwort aktualisieren", changePasswordPage);
+
+        // Update password
+        changePasswordPage.changePassword("password", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+    }
+
+
+    // KEYCLOAK-3887
+    @Test
+    public void languageChangeConsentScreen() {
+        // Set client, which requires consent
+        oauth.clientId("third-party");
+
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+
+        grantPage.assertCurrent();
+        Assert.assertEquals("English", grantPage.getLanguageDropdownText());
+
+        // Switch language
+        switchLanguageToGermanAndBack("Do you grant these access privileges?", "Wollen Sie diese Zugriffsrechte", changePasswordPage);
+
+        // Confirm grant
+        grantPage.accept();
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+        // Revert client
+        oauth.clientId("test-app");
+    }
+
+
+    private void switchLanguageToGermanAndBack(String expectedEnglishMessage, String expectedGermanMessage, LanguageComboboxAwarePage page) {
+        // Switch language to Deutsch
+        page.openLanguage("Deutsch");
+        Assert.assertEquals("Deutsch", page.getLanguageDropdownText());
+        String pageSource = driver.getPageSource();
+        Assert.assertFalse(pageSource.contains(expectedEnglishMessage));
+        Assert.assertTrue(pageSource.contains(expectedGermanMessage));
+
+        // Revert language
+        page.openLanguage("English");
+        Assert.assertEquals("English", page.getLanguageDropdownText());
+        pageSource = driver.getPageSource();
+        Assert.assertTrue(pageSource.contains(expectedEnglishMessage));
+        Assert.assertFalse(pageSource.contains(expectedGermanMessage));
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
index a72aa3a..20757c1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
@@ -444,14 +444,10 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
     
     private String generateS256CodeChallenge(String codeVerifier) throws Exception {
         MessageDigest md = MessageDigest.getInstance("SHA-256");
-        md.update(codeVerifier.getBytes());
-        StringBuilder sb = new StringBuilder();
-        for (byte b : md.digest()) {
-            String hex = String.format("%02x", b);
-            sb.append(hex);
-        }
-        String codeChallenge = Base64Url.encode(sb.toString().getBytes());
-    	return codeChallenge;
+        md.update(codeVerifier.getBytes("ISO_8859_1"));
+        byte[] digestBytes = md.digest();
+        String codeChallenge = Base64Url.encode(digestBytes);
+        return codeChallenge;
     }
  
     private void expectSuccessfulResponseFromTokenEndpoint(String codeId, String sessionId, String code)  throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
index b4f3130..842e406 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
@@ -18,7 +18,9 @@
 package org.keycloak.testsuite.util;
 
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -175,7 +177,15 @@ public class ClientBuilder {
     }
 
     public ClientBuilder authorizationServicesEnabled(boolean enable) {
-        rep.setAuthorizationServicesEnabled(true);
+        rep.setAuthorizationServicesEnabled(enable);
+        return this;
+    }
+
+    public ClientBuilder protocolMapper(ProtocolMapperRepresentation... mappers) {
+        if (rep.getProtocolMappers() == null) {
+            rep.setProtocolMappers(new ArrayList<>());
+        }
+        rep.getProtocolMappers().addAll(Arrays.asList(mappers));
         return this;
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-autodetect/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-autodetect/WEB-INF/keycloak-saml.xml
new file mode 100755
index 0000000..b00a1c0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-autodetect/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,44 @@
+<!--
+  ~ 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.
+  -->
+
+<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
+                       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                       xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_9.xsd">
+    <SP entityID="http://localhost:8081/sales-post/"
+        sslPolicy="EXTERNAL"
+        nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+        logoutPage="/logout.jsp"
+        forceAuthentication="false"
+	autodetectBearerOnly="true">
+        <PrincipalNameMapping policy="FROM_NAME_ID"/>
+        <RoleIdentifiers>
+            <Attribute name="Role"/>
+        </RoleIdentifiers>
+        <IDP entityID="idp">
+            <SingleSignOnService requestBinding="POST"
+                                 bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    />
+
+            <SingleLogoutService
+                    requestBinding="POST"
+                    responseBinding="POST"
+                    postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+                    />
+        </IDP>
+     </SP>
+</keycloak-saml-adapter>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 6bc040f..f9919d5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -49,7 +49,7 @@
     
     <container qualifier="auth-server-undertow" mode="suite" >
         <configuration>
-            <property name="enabled">${auth.server.undertow}</property>
+            <property name="enabled">${auth.server.undertow} &amp;&amp; ! ${auth.server.undertow.crossdc}</property>
             <property name="bindAddress">localhost</property>
             <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
             <property name="bindHttpPort">${auth.server.http.port}</property>
@@ -74,6 +74,7 @@
                 ${auth.server.feature}
             </property>
             <property name="javaVmArguments">
+                ${auth.server.jboss.jvm.debug.args}
                 ${auth.server.memory.settings}
                 -Djava.net.preferIPv4Stack=true
             </property>
@@ -103,6 +104,7 @@
                 <property name="outputToConsole">${backends.console.output}</property>
                 <property name="managementPort">${auth.server.backend1.management.port}</property>
                 <property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
+                <property name="bindHttpPortOffset">${auth.server.backend1.port.offset}</property>
             </configuration>
         </container>
         <container qualifier="auth-server-${auth.server}-backend2" mode="manual" >
@@ -124,6 +126,7 @@
                 <property name="outputToConsole">${backends.console.output}</property>
                 <property name="managementPort">${auth.server.backend2.management.port}</property>
                 <property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
+                <property name="bindHttpPortOffset">${auth.server.backend2.port.offset}</property>
             </configuration>
         </container>
     </group>
@@ -165,6 +168,162 @@
     </group>
 
 
+    <!-- Cross DC with embedded undertow. Node numbering is [centre #].[node #] -->
+    <group qualifier="auth-server-undertow-cross-dc">
+        <container qualifier="cache-server-cross-dc-1" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+                <property name="jbossHome">${cache.server.home}</property>
+                <property name="serverConfig">clustered.xml</property>
+                <property name="jbossArguments">
+                    -Djboss.socket.binding.port-offset=${cache.server.port.offset}
+                    -Djboss.default.multicast.address=234.56.78.99
+                    -Djboss.node.name=cache-server
+                    ${adapter.test.props}
+                    ${auth.server.profile}
+                </property>
+                <property name="javaVmArguments">
+                    ${auth.server.memory.settings}
+                    -Djava.net.preferIPv4Stack=true
+                </property>
+                <property name="outputToConsole">${cache.server.console.output}</property>
+                <property name="managementPort">${cache.server.management.port}</property>
+                <property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
+            </configuration>
+        </container>
+
+        <container qualifier="cache-server-cross-dc-2" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+                <property name="jbossHome">${cache.server.home}</property>
+                <property name="setupCleanServerBaseDir">true</property>
+                <property name="cleanServerBaseDir">${cache.server.home}/standalone-dc-2</property>
+                <property name="serverConfig">clustered.xml</property>
+                <property name="jbossArguments">
+                    -Djboss.socket.binding.port-offset=${cache.server.2.port.offset}
+                    -Djboss.default.multicast.address=234.56.78.99
+                    -Djboss.node.name=cache-server-dc-2
+                    ${adapter.test.props}
+                    ${auth.server.profile}
+                </property>
+                <property name="javaVmArguments">
+                    ${auth.server.memory.settings}
+                    -Djava.net.preferIPv4Stack=true
+                </property>
+                <property name="outputToConsole">${cache.server.console.output}</property>
+                <property name="managementPort">${cache.server.2.management.port}</property>
+                <property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
+            </configuration>
+        </container>
+
+        <container qualifier="auth-server-balancer-cross-dc" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer</property>
+                <property name="bindAddress">localhost</property>
+                <property name="bindHttpPort">${auth.server.http.port}</property>
+                <property name="nodes">auth-server-undertow-cross-dc-0.1=http://localhost:8101,auth-server-undertow-cross-dc-0.2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1.1=http://localhost:8111,auth-server-undertow-cross-dc-1.2-manual=http://localhost:8112</property>
+            </configuration>
+        </container>
+
+        <container qualifier="auth-server-undertow-cross-dc-0_1" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+                <property name="bindAddress">localhost</property>
+                <property name="bindHttpPort">${auth.server.http.port}</property>
+                <property name="bindHttpPortOffset">-79</property>
+                <property name="route">auth-server-undertow-cross-dc-0_1</property>
+                <property name="remoteMode">${undertow.remote}</property>
+                <property name="dataCenter">0</property>
+                <property name="keycloakConfigPropertyOverrides">{
+                    "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_1",
+                    "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
+                    "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+                    "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
+                    "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
+                    "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
+                    "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
+                }</property>
+            </configuration>
+        </container>
+        <container qualifier="auth-server-undertow-cross-dc-0_2-manual" mode="manual" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+                <property name="bindAddress">localhost</property>
+                <property name="bindHttpPort">${auth.server.http.port}</property>
+                <property name="bindHttpPortOffset">-78</property>
+                <property name="route">auth-server-undertow-cross-dc-0_2-manual</property>
+                <property name="remoteMode">${undertow.remote}</property>
+                <property name="dataCenter">0</property>
+                <property name="keycloakConfigPropertyOverrides">{
+                    "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_2-manual",
+                    "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
+                    "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+                    "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
+                    "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
+                    "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
+                    "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
+                }</property>
+            </configuration>
+        </container>
+
+        <container qualifier="auth-server-undertow-cross-dc-1_1" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+                <property name="bindAddress">localhost</property>
+                <property name="bindHttpPort">${auth.server.http.port}</property>
+                <property name="bindHttpPortOffset">-69</property>
+                <property name="route">auth-server-undertow-cross-dc-1_1</property>
+                <property name="remoteMode">${undertow.remote}</property>
+                <property name="dataCenter">1</property>
+                <property name="keycloakConfigPropertyOverrides">{
+                    "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_1",
+                    "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
+                    "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
+                    "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
+                    "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
+                    "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
+                    "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
+                }</property>
+            </configuration>
+        </container>
+        <container qualifier="auth-server-undertow-cross-dc-1_2-manual" mode="manual" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+                <property name="bindAddress">localhost</property>
+                <property name="bindHttpPort">${auth.server.http.port}</property>
+                <property name="bindHttpPortOffset">-68</property>
+                <property name="route">auth-server-undertow-cross-dc-1_2-manual</property>
+                <property name="remoteMode">${undertow.remote}</property>
+                <property name="dataCenter">1</property>
+                <property name="keycloakConfigPropertyOverrides">{
+                    "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_2-manual",
+                    "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
+                    "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
+                    "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
+                    "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
+                    "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
+                    "keycloak.connectionsJpa.driverDialect": "${keycloak.connectionsJpa.driverDialect.crossdc:}"
+                }</property>
+            </configuration>
+        </container>
+    </group>
+
+
     <container qualifier="auth-server-balancer-wildfly" mode="suite" >
         <configuration>
             <property name="enabled">${auth.server.cluster}</property>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 9d801e5..7a0e6b6 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -85,7 +85,7 @@
 
     "connectionsJpa": {
         "default": {
-            "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",
+            "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;MVCC=TRUE}",
             "driver": "${keycloak.connectionsJpa.driver:org.h2.Driver}",
             "driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
             "user": "${keycloak.connectionsJpa.user:sa}",
@@ -107,15 +107,23 @@
 
     "connectionsInfinispan": {
         "default": {
+            "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
+            "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:defaultNodeName}",
             "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
             "async": "${keycloak.connectionsInfinispan.async:false}",
             "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
             "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
             "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
-            "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
+            "remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
             "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
         }
     },
+    
+    "stickySessionEncoder": {
+        "infinispan": {
+            "nodeName": "${keycloak.stickySessionEncoder.nodeName,jboss.node.name:defaultNodeName}"
+        }
+    },
 
 
     "truststore": {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
index 5f84e38..fb1a7e0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
@@ -71,6 +71,50 @@
             }
         }
     ],
+    "groups": [
+        {
+            "name": "Group A",
+            "path": "/Group A",
+            "attributes": {
+                "topAttribute": [
+                    "true"
+                ]
+            },
+            "subGroups": [
+                {
+                    "name": "Group B",
+                    "path": "/Group A/Group B",
+                    "attributes": {
+                        "level2Attribute": [
+                            "true"
+                        ]
+                    },
+                    "subGroups": []
+                }
+            ]
+        },
+        {
+            "name": "Group C",
+            "path": "/Group C",
+            "attributes": {
+                "topAttribute": [
+                    "true"
+                ]
+            },
+            "subGroups": [
+                {
+                    "name": "Group D",
+                    "path": "/Group C/Group D",
+                    "attributes": {
+                        "level2Attribute": [
+                            "true"
+                        ]
+                    },
+                    "subGroups": []
+                }
+            ]
+        }
+    ],
     "users": [
         {
             "username": "wburke",
@@ -299,6 +343,14 @@
                         }
                     },
                     {
+                        "name": "Group Policy Test",
+                        "type": "group",
+                        "config": {
+                            "groupsClaim": "groups",
+                            "groups": "[{\"path\":\"/Group A\",\"extendChildren\":true},{\"path\":\"/Group A/Group B\",\"extendChildren\":false},{\"path\":\"/Group C/Group D\",\"extendChildren\":true}]"
+                        }
+                    },
+                    {
                         "name": "Only Premium User Policy",
                         "description": "Defines that only premium users can do something",
                         "type": "role",
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
index 30e7dba..67a494a 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/common/xslt/arquillian.xsl
@@ -39,6 +39,7 @@
                         ${adapter.test.props}
                     </property>
                     <property name="javaVmArguments">
+                        ${app.server.jboss.jvm.debug.args}
                         ${app.server.memory.settings}
                         -Djava.net.preferIPv4Stack=true
                     </property>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index e90f353..70533a9 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -52,6 +52,11 @@
         <app.server.startup.timeout>60</app.server.startup.timeout>
         <app.server.memory.settings>-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m</app.server.memory.settings>
 
+        <!--debug properties-->
+        <app.server.debug.port>5006</app.server.debug.port>
+        <app.server.debug.suspend>n</app.server.debug.suspend>
+        <app.server.jboss.jvm.debug.args>-agentlib:jdwp=transport=dt_socket,server=y,suspend=${app.server.debug.suspend},address=${app.server.host}:${app.server.debug.port}</app.server.jboss.jvm.debug.args>
+        
         <app.server.ssl.required>false</app.server.ssl.required>
 
         <app.server.reverse-proxy.port.offset>500</app.server.reverse-proxy.port.offset>
@@ -205,7 +210,8 @@
 
                                 <app.server.startup.timeout>${app.server.startup.timeout}</app.server.startup.timeout>
                                 <app.server.memory.settings>${app.server.memory.settings}</app.server.memory.settings>
-
+                                <app.server.jboss.jvm.debug.args>${app.server.jboss.jvm.debug.args}</app.server.jboss.jvm.debug.args>
+                                
                                 <app.server.reverse-proxy.port.offset>${app.server.reverse-proxy.port.offset}</app.server.reverse-proxy.port.offset>
 
                                 <app.server.1.port.offset>${app.server.1.port.offset}</app.server.1.port.offset>
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
new file mode 100644
index 0000000..2fd68f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 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.console.page.clients.authorization.policy;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicy implements PolicyTypeUI {
+
+    @Page
+    private GroupPolicyForm form;
+
+    public GroupPolicyForm form() {
+        return form;
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        return form.toRepresentation();
+    }
+
+    public void update(GroupPolicyRepresentation expected) {
+        form().populate(expected);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
new file mode 100644
index 0000000..389a214
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 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.console.page.clients.authorization.policy;
+
+import static org.openqa.selenium.By.tagName;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyForm extends Form {
+
+    @FindBy(id = "name")
+    private WebElement name;
+
+    @FindBy(id = "description")
+    private WebElement description;
+
+    @FindBy(id = "groupsClaim")
+    private WebElement groupsClaim;
+
+    @FindBy(id = "logic")
+    private Select logic;
+
+    @FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
+    private WebElement deleteButton;
+
+    @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
+    private WebElement confirmDelete;
+
+    @FindBy(id = "selectGroup")
+    private WebElement selectGroupButton;
+
+    @Drone
+    private WebDriver driver;
+
+    public void populate(GroupPolicyRepresentation expected) {
+        setInputValue(name, expected.getName());
+        setInputValue(description, expected.getDescription());
+        setInputValue(groupsClaim, expected.getGroupsClaim());
+        logic.selectByValue(expected.getLogic().name());
+
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : toRepresentation().getGroups()) {
+            boolean isExpected = false;
+
+            for (GroupPolicyRepresentation.GroupDefinition expectedDef : expected.getGroups()) {
+                if (definition.getPath().equals(expectedDef.getPath())) {
+                    isExpected = true;
+                    break;
+                }
+            }
+
+            if (!isExpected) {
+                unselect(definition.getPath());
+            }
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : expected.getGroups()) {
+            String path = definition.getPath();
+            String groupName = path.substring(path.lastIndexOf('/') + 1);
+            WebElement element = driver.findElement(By.xpath("//span[text()='" + groupName + "']"));
+            element.click();
+            selectGroupButton.click();
+            driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                    .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                    .map(webElement -> webElement.findElements(tagName("td")))
+                    .filter(tds -> tds.get(0).getText().equals(definition.getPath()))
+                    .forEach(tds -> {
+                        if (!tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                            if (definition.isExtendChildren()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        } else {
+                            if (!definition.isExtendChildren() && tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        }
+                    });
+        }
+
+        save();
+    }
+
+    private void unselect(String path) {
+        for (WebElement webElement : driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr"))) {
+            List<WebElement> tds = webElement.findElements(tagName("td"));
+
+            if (tds.size() > 1) {
+                if (tds.get(0).getText().equals(path)) {
+                    tds.get(2).findElement(By.tagName("button")).click();
+                    return;
+                }
+            }
+        }
+    }
+
+    public void delete() {
+        deleteButton.click();
+        confirmDelete.click();
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName(getInputValue(name));
+        representation.setDescription(getInputValue(description));
+        representation.setGroupsClaim(getInputValue(groupsClaim));
+        representation.setLogic(Logic.valueOf(logic.getFirstSelectedOption().getText().toUpperCase()));
+        representation.setGroups(new HashSet<>());
+
+        driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                .forEach(webElement -> {
+                    List<WebElement> tds = webElement.findElements(tagName("td"));
+                    representation.addGroupPath(tds.get(0).getText(), tds.get(1).findElement(By.tagName("input")).isSelected());
+                });
+
+        return representation;
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
index af2a540..7be563e 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
@@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
@@ -66,6 +67,9 @@ public class Policies extends Form {
     @Page
     private ClientPolicy clientPolicy;
 
+    @Page
+    private GroupPolicy groupPolicy;
+
     public PoliciesTable policies() {
         return table;
     }
@@ -103,6 +107,10 @@ public class Policies extends Form {
             clientPolicy.form().populate((ClientPolicyRepresentation) expected);
             clientPolicy.form().save();
             return (P) clientPolicy;
+        } else if ("group".equals(type)) {
+            groupPolicy.form().populate((GroupPolicyRepresentation) expected);
+            groupPolicy.form().save();
+            return (P) groupPolicy;
         }
 
         return null;
@@ -130,6 +138,8 @@ public class Policies extends Form {
                     rulePolicy.form().populate((RulePolicyRepresentation) representation);
                 } else if ("client".equals(type)) {
                     clientPolicy.form().populate((ClientPolicyRepresentation) representation);
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().populate((GroupPolicyRepresentation) representation);
                 }
 
                 return;
@@ -158,6 +168,8 @@ public class Policies extends Form {
                     return (P) rulePolicy;
                 } else if ("client".equals(type)) {
                     return (P) clientPolicy;
+                } else if ("group".equals(type)) {
+                    return (P) groupPolicy;
                 }
             }
         }
@@ -187,6 +199,8 @@ public class Policies extends Form {
                     rulePolicy.form().delete();
                 } else if ("client".equals(type)) {
                     clientPolicy.form().delete();
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().delete();
                 }
 
                 return;
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..e8b05bf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2017 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.console.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.GroupPolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.RolePolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.UserPolicy;
+import org.keycloak.testsuite.util.GroupBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest {
+
+    @Before
+    public void configureTest() {
+        super.configureTest();
+        RealmResource realmResource = testRealmResource();
+        String groupAId = ApiUtil.getCreatedId(realmResource.groups().add(GroupBuilder.create().name("Group A").build()));
+        String groupBId = ApiUtil.getCreatedId(realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group B").build()));
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group D").build());
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group E").build());
+        realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group C").build());
+        realmResource.groups().add(GroupBuilder.create().name("Group F").build());
+    }
+
+    @Test
+    public void testUpdate() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+
+        String previousName = expected.getName();
+
+        expected.setName("Changed Test Group Policy");
+        expected.setDescription("Changed description");
+        expected.setLogic(Logic.NEGATIVE);
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(previousName, expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        GroupPolicy actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group A", false);
+        expected.addGroupPath("/Group A/Group B/Group D");
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group E");
+        expected.addGroupPath("/Group A/Group B", true);
+        expected.addGroupPath("/Group A/Group C");
+
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+    }
+
+    @Test
+    public void testDelete() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Delete Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().delete(expected.getName());
+        assertAlertSuccess();
+        authorizationPage.navigateTo();
+        assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+    }
+
+    private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) {
+        GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
+        assertAlertSuccess();
+        return assertPolicy(expected, policy);
+    }
+
+    private GroupPolicyRepresentation assertPolicy(GroupPolicyRepresentation expected, GroupPolicy policy) {
+        GroupPolicyRepresentation actual = policy.toRepresentation();
+
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.getLogic(), actual.getLogic());
+
+        assertNotNull(actual.getGroups());
+        assertEquals(expected.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !expected.getGroups().stream()
+                .filter(groupDefinition -> actualDefinition.getPath().contains(groupDefinition.getPath()) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+        return actual;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 4e29ec3..24e67c9 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -41,7 +41,9 @@
     <properties>
         <auth.server>undertow</auth.server>
         <auth.server.undertow>true</auth.server.undertow>
-
+        <auth.server.undertow.crossdc>false</auth.server.undertow.crossdc>
+        <auth.server.crossdc>false</auth.server.crossdc>
+        
         <auth.server.container>auth-server-${auth.server}</auth.server.container>
         <auth.server.home>${containers.home}/${auth.server.container}</auth.server.home>
         <auth.server.config.dir>${auth.server.home}</auth.server.config.dir>
@@ -62,10 +64,28 @@
         <auth.server.jboss.skip.unpack>${auth.server.undertow}</auth.server.jboss.skip.unpack>
         <auth.server.jboss.startup.timeout>300</auth.server.jboss.startup.timeout>
         
+        <!--debug properties-->
+        <auth.server.debug.port>5005</auth.server.debug.port>
+        <auth.server.debug.suspend>n</auth.server.debug.suspend>
+        <auth.server.jboss.jvm.debug.args>-agentlib:jdwp=transport=dt_socket,server=y,suspend=${auth.server.debug.suspend},address=${auth.server.host}:${auth.server.debug.port}</auth.server.jboss.jvm.debug.args>
+        
         <auth.server.remote>false</auth.server.remote>
         <auth.server.profile/>
         <auth.server.feature/>
-    
+
+        <cache.server>undefined</cache.server>
+        <cache.server.container>cache-server-${cache.server}</cache.server.container>
+        <cache.server.home>${containers.home}/${cache.server.container}</cache.server.home>
+        <cache.server.port.offset>1010</cache.server.port.offset>
+        <cache.server.management.port>11000</cache.server.management.port>
+        <cache.server.2.port.offset>2010</cache.server.2.port.offset>
+        <cache.server.2.management.port>12000</cache.server.2.management.port>
+        <cache.server.console.output>true</cache.server.console.output>
+        <keycloak.connectionsInfinispan.remoteStoreServer>localhost</keycloak.connectionsInfinispan.remoteStoreServer>
+        <keycloak.connectionsInfinispan.remoteStorePort>12232</keycloak.connectionsInfinispan.remoteStorePort>
+        <keycloak.connectionsInfinispan.remoteStorePort.2>13232</keycloak.connectionsInfinispan.remoteStorePort.2>
+        <keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
+
         <adapter.test.props/>
         <migration.import.properties/>
         <examples.home>${project.build.directory}/examples</examples.home>
@@ -165,6 +185,23 @@
                     </executions>
                 </plugin>
                 <plugin>
+                    <artifactId>maven-antrun-plugin</artifactId>
+                    <executions>
+                        <execution>
+                            <id>clean-second-cache-server-arquillian-bug-workaround</id>
+                            <phase>process-test-resources</phase>
+                            <goals><goal>run</goal></goals>
+                            <configuration>
+                                <target>
+                                    <echo>${cache.server.home}/standalone-dc-2</echo>
+                                    <delete failonerror="false" dir="${cache.server.home}/standalone-dc-2" />
+                                    <mkdir dir="${cache.server.home}/standalone-dc-2/deployments" />
+                                </target>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
                     <artifactId>maven-surefire-plugin</artifactId>
                     <configuration>
                         <systemPropertyVariables>
@@ -194,6 +231,7 @@
                             <auth.server.config.property.name>${auth.server.config.property.name}</auth.server.config.property.name>
                             <auth.server.config.property.value>${auth.server.config.property.value}</auth.server.config.property.value>
                             <auth.server.adapter.impl.class>${auth.server.adapter.impl.class}</auth.server.adapter.impl.class>
+                            <auth.server.jboss.jvm.debug.args>${auth.server.jboss.jvm.debug.args}</auth.server.jboss.jvm.debug.args>
                             
                             <auth.server.profile>${auth.server.profile}</auth.server.profile>
                             <auth.server.feature>${auth.server.feature}</auth.server.feature>
@@ -229,6 +267,25 @@
                             <client.key.passphrase>${client.key.passphrase}</client.key.passphrase>
 
                             <auth.server.ocsp.responder.enabled>${auth.server.ocsp.responder.enabled}</auth.server.ocsp.responder.enabled>
+                            
+                            <!--cache server properties-->
+                            <auth.server.crossdc>${auth.server.crossdc}</auth.server.crossdc>
+                            <auth.server.undertow.crossdc>${auth.server.undertow.crossdc}</auth.server.undertow.crossdc>
+
+                            <cache.server>${cache.server}</cache.server>
+                            <cache.server.port.offset>${cache.server.port.offset}</cache.server.port.offset>
+                            <cache.server.container>${cache.server.container}</cache.server.container>
+                            <cache.server.home>${cache.server.home}</cache.server.home>
+                            <cache.server.console.output>${cache.server.console.output}</cache.server.console.output>
+                            <cache.server.management.port>${cache.server.management.port}</cache.server.management.port>
+                            <cache.server.2.port.offset>${cache.server.2.port.offset}</cache.server.2.port.offset>
+                            <cache.server.2.management.port>${cache.server.2.management.port}</cache.server.2.management.port>
+
+                            <keycloak.connectionsInfinispan.remoteStorePort>${keycloak.connectionsInfinispan.remoteStorePort}</keycloak.connectionsInfinispan.remoteStorePort>
+                            <keycloak.connectionsInfinispan.remoteStorePort.2>${keycloak.connectionsInfinispan.remoteStorePort.2}</keycloak.connectionsInfinispan.remoteStorePort.2>
+                            <keycloak.connectionsInfinispan.remoteStoreServer>${keycloak.connectionsInfinispan.remoteStoreServer}</keycloak.connectionsInfinispan.remoteStoreServer>
+
+                            <keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
                         </systemPropertyVariables>
                         <properties>
                             <property>
@@ -289,6 +346,7 @@
                 <auth.server>wildfly</auth.server>
                 <auth.server.jboss>true</auth.server.jboss>
                 <auth.server.undertow>false</auth.server.undertow>
+                <auth.server.undertow.crossdc>false</auth.server.undertow.crossdc>
                 <auth.server.config.property.value>standalone.xml</auth.server.config.property.value>
                 <auth.server.config.dir>${auth.server.home}/standalone/configuration</auth.server.config.dir>
                 <h2.version>1.3.173</h2.version>
@@ -307,6 +365,7 @@
                 <auth.server>eap</auth.server>
                 <auth.server.jboss>true</auth.server.jboss>
                 <auth.server.undertow>false</auth.server.undertow>
+                <auth.server.undertow.crossdc>false</auth.server.undertow.crossdc>
                 <auth.server.config.property.value>standalone.xml</auth.server.config.property.value>
                 <auth.server.config.dir>${auth.server.home}/standalone/configuration</auth.server.config.dir>
                 <h2.version>1.3.173</h2.version>
@@ -320,6 +379,154 @@
         </profile>
 
         <profile>
+            <id>cache-server-infinispan</id>
+            <properties>
+                <cache.server>infinispan</cache.server>
+                <auth.server.undertow.crossdc>true</auth.server.undertow.crossdc>
+                <auth.server.crossdc>true</auth.server.crossdc>
+                <cache.server.jboss>true</cache.server.jboss>
+                <cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>                
+            </properties>
+            <dependencies>
+                <dependency>
+                    <groupId>org.wildfly.arquillian</groupId>
+                    <artifactId>wildfly-arquillian-container-managed</artifactId>
+                </dependency>
+            </dependencies>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>enforce</goal>
+                                </goals>
+                                <configuration>
+                                    <rules>
+                                        <!--requireActiveProfile 'auth-server-wildfly/eap' doesn't work unless the profiles are defined in all submodule poms
+                                        using requireProperty instead-->
+                                        <requireProperty>
+                                            <property>cache.server</property>
+                                            <regex>(infinispan)|(jdg)</regex>
+                                            <regexMessage>Profile "cache-server-infinispan" requires activation of profile "cache-server-infinispan" or "cache-server-jdg".</regexMessage>
+                                        </requireProperty>
+                                    </rules>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                    </plugin>
+                </plugins>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <artifactId>maven-dependency-plugin</artifactId>
+                            <executions>
+                                <execution>
+                                    <id>unpack-cache-server-infinispan</id>
+                                    <phase>generate-resources</phase>
+                                    <goals>
+                                        <goal>unpack</goal>
+                                    </goals>
+                                    <configuration>
+                                        <artifactItems>
+                                            <artifactItem>
+                                                <groupId>org.keycloak.testsuite</groupId>
+                                                <artifactId>integration-arquillian-servers-cache-server-infinispan</artifactId>
+                                                <version>${project.version}</version>
+                                                <type>zip</type>
+                                                <outputDirectory>${containers.home}</outputDirectory>
+                                            </artifactItem>
+                                        </artifactItems>
+                                        <overWriteIfNewer>true</overWriteIfNewer>
+                                    </configuration>
+                                </execution>
+                            </executions>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
+        </profile>
+        
+        <profile>
+            <id>cache-server-jdg</id>
+            <properties>
+                <cache.server>jdg</cache.server>
+                <auth.server.undertow.crossdc>true</auth.server.undertow.crossdc>
+                <auth.server.crossdc>true</auth.server.crossdc>
+                <cache.server.jboss>true</cache.server.jboss>
+                <cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>
+            </properties>
+            <dependencies>
+                <dependency>
+                    <groupId>org.wildfly.arquillian</groupId>
+                    <artifactId>wildfly-arquillian-container-managed</artifactId>
+                </dependency>
+            </dependencies>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>enforce</goal>
+                                </goals>
+                                <configuration>
+                                    <rules>
+                                        <!--requireActiveProfile 'auth-server-wildfly/eap' doesn't work unless the profiles are defined in all submodule poms
+                                        using requireProperty instead-->
+                                        <requireProperty>
+                                            <property>cache.server</property>
+                                            <regex>(infinispan)|(jdg)</regex>
+                                            <regexMessage>Profile "cache-server-jdg" requires activation of profile "cache-server-infinispan" or "cache-server-jdg".</regexMessage>
+                                        </requireProperty>
+                                    </rules>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                    </plugin>
+                </plugins>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <artifactId>maven-dependency-plugin</artifactId>
+                            <executions>
+                                <execution>
+                                    <id>unpack-cache-server-jdg</id>
+                                    <phase>generate-resources</phase>
+                                    <goals>
+                                        <goal>unpack</goal>
+                                    </goals>
+                                    <configuration>
+                                        <artifactItems>
+                                            <artifactItem>
+                                                <groupId>org.keycloak.testsuite</groupId>
+                                                <artifactId>integration-arquillian-servers-cache-server-jdg</artifactId>
+                                                <version>${project.version}</version>
+                                                <type>zip</type>
+                                                <outputDirectory>${containers.home}</outputDirectory>
+                                            </artifactItem>
+                                        </artifactItems>
+                                        <overWriteIfNewer>true</overWriteIfNewer>
+                                    </configuration>
+                                </execution>
+                            </executions>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
+        </profile>
+
+        <profile>
             <id>auth-server-profile</id>
             <activation>
                 <property>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index af76c9c..1a0de6e 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1209,6 +1209,13 @@ authz-policy-js-code.tooltip=The JavaScript code providing the conditions for th
 authz-aggregated=Aggregated
 authz-add-aggregated-policy=Add Aggregated Policy
 
+# Authz Group Policy Detail
+authz-add-group-policy=Add Group Policy
+authz-no-groups-assigned=No groups assigned.
+authz-policy-group-claim=Groups Claim
+authz-policy-group-claim.tooltip=A claim to use as the source for user’s group. If the claim is present it must be an array of strings.
+authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
+
 # Authz Permission List
 authz-no-permissions-available=No permissions available.
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
index 7c5e7fa..2b92bc5 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -324,7 +324,29 @@ module.config(['$routeProvider', function ($routeProvider) {
             }
         },
         controller: 'ResourceServerPolicyRoleDetailCtrl'
-    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
+    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/create', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/:id', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
         templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
         resolve: {
             realm: function (RealmLoader) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 2cce138..89d0a48 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -367,6 +367,9 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
     }
 
     this.checkNameAvailability = function (onSuccess) {
+        if (!$scope.resource.name || $scope.resource.name.trim().length == 0) {
+            return;
+        }
         ResourceServerResource.search({
             realm : $route.current.params.realm,
             client : client.id,
@@ -593,6 +596,9 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout
     }
 
     this.checkNameAvailability = function (onSuccess) {
+        if (!$scope.scope.name || $scope.scope.name.trim().length == 0) {
+            return;
+        }
         ResourceServerScope.search({
             realm : $route.current.params.realm,
             client : client.id,
@@ -1728,6 +1734,119 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route,
     }
 });
 
+module.controller('ResourceServerPolicyGroupDetailCtrl', function($scope, $route, realm, client, Client, Groups, Group, PolicyController) {
+    PolicyController.onInit({
+        getPolicyType : function() {
+            return "group";
+        },
+
+        onInit : function() {
+            $scope.tree = [];
+
+            Groups.query({realm: $route.current.params.realm}, function(groups) {
+                $scope.groups = groups;
+                $scope.groupList = [
+                    {"id" : "realm", "name": "Groups",
+                                "subGroups" : groups}
+                ];
+            });
+
+            var isLeaf = function(node) {
+                return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+            }
+
+            $scope.getGroupClass = function(node) {
+                if (node.id == "realm") {
+                    return 'pficon pficon-users';
+                }
+                if (isLeaf(node)) {
+                    return 'normal';
+                }
+                if (node.subGroups.length && node.collapsed) return 'collapsed';
+                if (node.subGroups.length && !node.collapsed) return 'expanded';
+                return 'collapsed';
+
+            }
+
+            $scope.getSelectedClass = function(node) {
+                if (node.selected) {
+                    return 'selected';
+                } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+                    return 'cut';
+                }
+                return undefined;
+            }
+
+            $scope.selectGroup = function(group) {
+                for (i = 0; i < $scope.selectedGroups.length; i++) {
+                    if ($scope.selectedGroups[i].id == group.id) {
+                        return
+                    }
+                }
+                $scope.selectedGroups.push({id: group.id, path: group.path});
+                $scope.changed = true;
+            }
+
+            $scope.extendChildren = function(group) {
+                $scope.changed = true;
+            }
+
+            $scope.removeFromList = function(group) {
+                var index = $scope.selectedGroups.indexOf(group);
+                if (index != -1) {
+                    $scope.selectedGroups.splice(index, 1);
+                    $scope.changed = true;
+                }
+            }
+        },
+
+        onInitCreate : function(policy) {
+            var selectedGroups = [];
+
+            $scope.selectedGroups = angular.copy(selectedGroups);
+
+            $scope.$watch('selectedGroups', function() {
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onInitUpdate : function(policy) {
+            $scope.selectedGroups = policy.groups;
+
+            angular.forEach($scope.selectedGroups, function(group, index){
+               Group.get({realm: $route.current.params.realm, groupId: group.id}, function (existing) {
+                   group.path = existing.path;
+               });
+            });
+
+            $scope.$watch('selectedGroups', function() {
+                if (!$scope.changed) {
+                    return;
+                }
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onUpdate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        },
+
+        onCreate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        }
+    }, realm, client, $scope);
+});
+
 module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
     PolicyController.onInit({
         getPolicyType : function() {
@@ -2060,6 +2179,9 @@ module.service("PolicyController", function($http, $route, $location, ResourceSe
         }
 
         this.checkNameAvailability = function (onSuccess) {
+            if (!$scope.policy.name || $scope.policy.name.trim().length == 0) {
+                return;
+            }
             ResourceServerPolicy.search({
                 realm: $route.current.params.realm,
                 client: client.id,
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
new file mode 100644
index 0000000..61af0f1
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
@@ -0,0 +1,124 @@
+<!--
+  ~ * Copyright 2017 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.
+  -->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
+        <li data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</li>
+        <li data-ng-hide="create">{{:: 'groups' | translate}}</li>
+        <li data-ng-hide="create">{{originalPolicy.name}}</li>
+    </ol>
+
+    <h1 data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</h1>
+    <h1 data-ng-hide="create">{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+                                                         data-ng-click="remove()"></i></h1>
+
+    <form class="form-horizontal" name="groupPolicyForm" novalidate>
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required data-ng-blur="checkNewNameAvailability()">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="groupsClaim">{{:: 'authz-policy-group-claim' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="groupsClaim" name="groupsClaim" data-ng-model="policy.groupsClaim" required>
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-group-claim.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="selectedGroups">{{:: 'groups' | translate}} <span class="required">*</span></label>
+                <div class="col-md-6">
+                    <div tree-id="tree"
+                        angular-treeview="true"
+                        tree-model="groupList"
+                        node-id="id"
+                        node-label="name"
+                        node-children="subGroups" >
+                    </div>
+                    <button data-ng-click="selectGroup(tree.currentNode)" id="selectGroup" class="btn btn-primary" data-ng-disabled="tree.currentNode == null">Select</button>
+                    <input class="form-control" type="text" id="selectedGroups" name="selectedGroups" data-ng-model="noop" data-ng-required="selectedGroups.length <= 0" autofocus required data-ng-show="false">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-user-users.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-if="selectedGroups.length > 0">
+                <label class="col-md-2 control-label"></label>
+                <div class="col-md-5">
+                    <table class="table table-striped table-bordered" id="selected-groups">
+                        <thead>
+                            <tr>
+                                <th>{{:: 'path' | translate}}</th>
+                                <th class="col-sm-3">Extend to Children</th>
+                                <th>{{:: 'actions' | translate}}</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr ng-repeat="group in selectedGroups | orderBy:'name' track by $index">
+                                <td>{{group.path}}</td>
+                                <td>
+                                    <input type="checkbox" ng-model="group.extendChildren" id="{{role.id}}" data-ng-click="extendChildren()">
+                                </td>
+                                <td class="kc-action-cell">
+                                    <button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(group);">{{:: 'remove' | translate}}</button>
+                                </td>
+                            </tr>
+                            <tr data-ng-show="!selectedGroups.length">
+                                <td class="text-muted" colspan="3">{{:: 'authz-no-groups-assigned' | translate}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="logic">{{:: 'authz-policy-logic' | translate}}</label>
+
+                <div class="col-sm-1">
+                    <select class="form-control" id="logic"
+                            data-ng-model="policy.logic">
+                        <option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
+                        <option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
+                    </select>
+                </div>
+
+                <kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <input type="hidden" data-ng-model="policy.type"/>
+        </fieldset>
+        <div class="form-group" data-ng-show="access.manageAuthorization">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+                <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file