keycloak-uncached

Merge pull request #1191 from Smartling/KEYCLOAK-1238 Add

4/28/2015 10:49:33 PM

Changes

Details

diff --git a/integration/pom.xml b/integration/pom.xml
index 1680a08..d19ad02 100755
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -31,5 +31,6 @@
         <module>admin-client</module>
         <module>osgi-adapter</module>
         <module>spring-boot</module>
+        <module>spring-security</module>
     </modules>
 </project>
diff --git a/integration/spring-security/pom.xml b/integration/spring-security/pom.xml
new file mode 100755
index 0000000..8c13319
--- /dev/null
+++ b/integration/spring-security/pom.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.2.0.RC1-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-spring-security-adapter</artifactId>
+    <name>Keycloak Spring Security Integration</name>
+    <description/>
+
+    <properties>
+        <spring.version>3.2.7.RELEASE</spring.version>
+        <spring-security.version>3.2.7.RELEASE</spring-security.version>
+        <mockito.version>1.9.5</mockito.version>
+        <apache-httpcomponents.version>4.3.6</apache-httpcomponents.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-config</artifactId>
+            <version>${spring-security.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-web</artifactId>
+            <version>${spring-security.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>${apache-httpcomponents.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>net.iharder</groupId>
+            <artifactId>base64</artifactId>
+            <version>${base64.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>${bouncycastle.crypto.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+            <version>${jackson.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+            <version>${jackson.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-xc</artifactId>
+            <version>${jackson.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <version>${spring.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+            <version>${jboss.logging.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java
new file mode 100644
index 0000000..15108ea
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/KeycloakRole.java
@@ -0,0 +1,62 @@
+package org.keycloak.adapters.springsecurity.account;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
+
+/**
+ * Represents an authority granted to an {@link Authentication} by the Keycloak server.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakRole implements GrantedAuthority {
+
+    private String role;
+
+    /**
+     * Creates a new granted authority from the given Keycloak role.
+     *
+     * @param role the name of this granted authority
+     */
+    public KeycloakRole(String role) {
+        Assert.notNull(role, "role cannot be null");
+        this.role = role;
+    }
+
+    @Override
+    public String getAuthority() {
+        return role;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof KeycloakRole)) {
+            return false;
+        }
+
+        KeycloakRole that = (KeycloakRole) o;
+
+        if (!role.equals(that.role)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return 3 * role.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "KeycloakRole{" +
+                "role='" + role + '\'' +
+                '}';
+    }
+
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java
new file mode 100644
index 0000000..338a72f
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/account/SimpleKeycloakAccount.java
@@ -0,0 +1,42 @@
+package org.keycloak.adapters.springsecurity.account;
+
+import org.keycloak.adapters.KeycloakAccount;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.Set;
+
+/**
+ * Concrete, serializable {@link KeycloakAccount} implementation.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class SimpleKeycloakAccount implements KeycloakAccount, Serializable {
+
+    private Set<String> roles;
+    private Principal principal;
+    private RefreshableKeycloakSecurityContext securityContext;
+
+    public SimpleKeycloakAccount(Principal principal, Set<String> roles,  RefreshableKeycloakSecurityContext securityContext) {
+        this.principal = principal;
+        this.roles = roles;
+        this.securityContext = securityContext;
+    }
+
+    @Override
+    public Principal getPrincipal() {
+        return principal;
+    }
+
+    @Override
+    public Set<String> getRoles() {
+        return roles;
+    }
+
+    @Override
+    public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
+        return securityContext;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java
new file mode 100644
index 0000000..29433ef
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java
@@ -0,0 +1,58 @@
+package org.keycloak.adapters.springsecurity;
+
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.Resource;
+
+import java.io.InputStream;
+
+/**
+ * Bean holding the {@link KeycloakDeployment} and {@link AdapterDeploymentContext} for this
+ * Spring application context. The Keycloak deployment is loaded from the required
+ * <code>WEB-INF/keycloak.json</code> file generated by Keycloak.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class AdapterDeploymentContextBean implements ApplicationContextAware, InitializingBean {
+
+    private ApplicationContext applicationContext;
+    private AdapterDeploymentContext deploymentContext;
+    private KeycloakDeployment deployment;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        Resource resource = applicationContext.getResource("WEB-INF/keycloak.json");
+        InputStream is = resource.getInputStream();
+        this.deployment = KeycloakDeploymentBuilder.build(is);
+        this.deploymentContext = new AdapterDeploymentContext(deployment);
+    }
+
+    /**
+     * Returns the Keycloak {@link AdapterDeploymentContext} for this application context.
+     *
+     * @return the Keycloak {@link AdapterDeploymentContext} for this application context
+     */
+    public AdapterDeploymentContext getDeploymentContext() {
+        return deploymentContext;
+    }
+
+    /**
+     * Returns the {@link KeycloakDeployment} for this application context.
+     *
+     * @return the {@link KeycloakDeployment} for this application context
+     */
+    public KeycloakDeployment getDeployment() {
+        return deployment;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java
new file mode 100644
index 0000000..3357806
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java
@@ -0,0 +1,45 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.util.Assert;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+    /**
+     * Default Keycloak authentication login URI
+     */
+    public static final String DEFAULT_LOGIN_URI = "/sso/login";
+
+    private final static Logger log = LoggerFactory.getLogger(KeycloakAuthenticationEntryPoint.class);
+
+    private String loginUri = DEFAULT_LOGIN_URI;
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
+            throws IOException, ServletException {
+
+        String contextAwareLoginUri = request.getContextPath() + loginUri;
+
+        log.debug("Redirecting to login URI {}", contextAwareLoginUri);
+        response.sendRedirect(contextAwareLoginUri);
+    }
+
+    public void setLoginUri(String loginUri) {
+        Assert.notNull(loginUri, "loginUri cannot be null");
+        this.loginUri = loginUri;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java
new file mode 100644
index 0000000..4e50685
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProvider.java
@@ -0,0 +1,38 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.keycloak.adapters.springsecurity.account.KeycloakRole;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Performs authentication on a {@link KeycloakAuthenticationToken}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakAuthenticationProvider implements AuthenticationProvider {
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+        KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
+        List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
+
+        for (String role : token.getAccount().getRoles()) {
+            grantedAuthorities.add(new KeycloakRole(role));
+        }
+
+        return new KeycloakAuthenticationToken(token.getAccount(), grantedAuthorities);
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return KeycloakAuthenticationToken.class.isAssignableFrom(aClass);
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java
new file mode 100644
index 0000000..2879a06
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java
@@ -0,0 +1,69 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.util.Assert;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Logs the current user out of Keycloak.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakLogoutHandler implements LogoutHandler {
+
+    public static final String SSO_LOGOUT_COMPLETE_PARAM = "sso_complete";
+    private static final Logger log = LoggerFactory.getLogger(KeycloakLogoutHandler.class);
+
+    private AdapterDeploymentContextBean deploymentContextBean;
+
+    public KeycloakLogoutHandler(AdapterDeploymentContextBean deploymentContextBean) {
+        Assert.notNull(deploymentContextBean);
+        this.deploymentContextBean = deploymentContextBean;
+    }
+
+    @Override
+    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+
+        if (authentication instanceof AnonymousAuthenticationToken) {
+            log.warn("Attempt to log out an anonymous authentication");
+            return;
+        }
+
+        if (Boolean.valueOf(request.getParameter(SSO_LOGOUT_COMPLETE_PARAM))) {
+            // already logged out
+            return;
+        }
+
+        try {
+            handleSingleSignOut(request, response);
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to redirect to SSO url!", e);
+        }
+
+    }
+
+    protected String createRedirectUrl(HttpServletRequest request) {
+
+        return UriComponentsBuilder.fromHttpUrl(request.getRequestURL().toString())
+                .replaceQueryParam(SSO_LOGOUT_COMPLETE_PARAM, true).build().toUriString();
+    }
+
+    protected void handleSingleSignOut(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+        KeycloakDeployment deployment = deploymentContextBean.getDeployment();
+        String redirectUrl = createRedirectUrl(request);
+
+        response.sendRedirect(deployment.getLogoutUrl().queryParam("redirect_uri", redirectUrl).build().toASCIIString());
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java
new file mode 100644
index 0000000..8b229ea
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java
@@ -0,0 +1,91 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.keycloak.KeycloakPrincipal;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.AdapterTokenStore;
+import org.keycloak.adapters.AdapterUtils;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakAccount;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OAuthRequestAuthenticator;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.security.Principal;
+import java.util.Set;
+import java.util.logging.Level;
+
+/**
+ * Request authenticator adapter for Spring Security.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class SpringSecurityRequestAuthenticator extends RequestAuthenticator {
+
+    private static final Logger logger = LoggerFactory.getLogger(SpringSecurityRequestAuthenticator.class);
+    private final HttpServletRequest request;
+
+    /**
+     * Creates a new Spring Security request authenticator.
+     *
+     * @param facade the current <code>HttpFacade</code> (required)
+     * @param request the current <code>HttpServletRequest</code> (required)
+     * @param deployment the <code>KeycloakDeployment</code> (required)
+     * @param tokenStore the <cdoe>AdapterTokenStore</cdoe> (required)
+     * @param sslRedirectPort the SSL redirect port (required)
+     */
+    public SpringSecurityRequestAuthenticator(
+            HttpFacade facade,
+            HttpServletRequest request,
+            KeycloakDeployment deployment,
+            AdapterTokenStore tokenStore,
+            int sslRedirectPort) {
+
+        super(facade, deployment, tokenStore, sslRedirectPort);
+        this.request = request;
+    }
+
+    @Override
+    protected OAuthRequestAuthenticator createOAuthAuthenticator() {
+        return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore);
+    }
+
+    @Override
+    protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
+
+        final RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
+        final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
+        final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
+
+        request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
+        this.tokenStore.saveAccountInfo(account);
+    }
+
+    @Override
+    protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method) {
+
+        RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
+        Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
+        final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
+
+        logger.warn("Completing bearer authentication. Bearer roles: {} ",roles);
+
+        SecurityContextHolder.getContext().setAuthentication(new KeycloakAuthenticationToken(account));
+        request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
+    }
+
+    @Override
+    protected String getHttpSessionId(boolean create) {
+        HttpSession session = request.getSession(create);
+        return session != null ? session.getId() : null;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java
new file mode 100644
index 0000000..2dea071
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java
@@ -0,0 +1,45 @@
+package org.keycloak.adapters.springsecurity.client;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.HttpClients;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.context.annotation.Scope;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+/**
+ * Factory for {@link ClientHttpRequest} objects created for server to server secured
+ * communication using OAuth2 bearer tokens issued by Keycloak.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+@Component
+@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+public class KeycloakClientRequestFactory extends HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory {
+
+    public static final String AUTHORIZATION_HEADER = "Authorization";
+
+    public KeycloakClientRequestFactory() {
+        super(HttpClients.custom()
+                .disableCookieManagement()
+                .build()
+        );
+    }
+
+    @Override
+    protected void postProcessHttpRequest(HttpUriRequest request) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
+        KeycloakSecurityContext context = token.getAccount().getKeycloakSecurityContext();
+
+        request.setHeader(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString());
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java
new file mode 100644
index 0000000..3451717
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java
@@ -0,0 +1,17 @@
+package org.keycloak.adapters.springsecurity.client;
+
+import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Created by scott on 4/22/15.
+ */
+//@Service
+//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+public class KeycloakRestTemplate extends RestTemplate implements RestOperations {
+    public KeycloakRestTemplate(KeycloakClientRequestFactory factory) {
+        super(factory);
+    }
+
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java
new file mode 100644
index 0000000..7a3d772
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java
@@ -0,0 +1,84 @@
+package org.keycloak.adapters.springsecurity.config;
+
+import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
+import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
+import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
+import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler;
+import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
+import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
+import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+
+/**
+ * Provides a convenient base class for creating a {@link WebSecurityConfigurer}
+ * instance secured by Keycloak. This implementation allows customization by overriding methods.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ *
+ * @see EnableWebSecurity
+ * @see EnableWebMvcSecurity
+ */
+public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
+
+    @Bean
+    protected AdapterDeploymentContextBean adapterDeploymentContextBean() {
+        return new AdapterDeploymentContextBean();
+    }
+
+    @Bean
+    protected AuthenticationEntryPoint authenticationEntryPoint()
+    {
+        return new KeycloakAuthenticationEntryPoint();
+    }
+
+    @Bean
+    protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
+        return new KeycloakAuthenticationProvider();
+    }
+
+    @Bean
+    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
+        KeycloakAuthenticationProcessingFilter filter  = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
+        filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
+        return filter;
+    }
+
+    @Bean
+    protected KeycloakPreAuthActionsFilter keycloakPreAuthActionsFilter() {
+        return new KeycloakPreAuthActionsFilter(httpSessionManager());
+    }
+
+    @Bean
+    protected HttpSessionManager httpSessionManager() {
+        return  new HttpSessionManager();
+    }
+
+    @Bean
+    protected KeycloakLogoutHandler keycloakLogoutHandler() {
+        return new KeycloakLogoutHandler(adapterDeploymentContextBean());
+    }
+
+    protected abstract SessionAuthenticationStrategy sessionAuthenticationStrategy();
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http
+                .sessionManagement()
+                .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
+                .and()
+                .addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
+                .addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class)
+                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java
new file mode 100644
index 0000000..586dcba
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides an {@link org.keycloak.adapters.HttpFacade} implementation.
+ */
+package org.keycloak.adapters.springsecurity.facade;
\ No newline at end of file
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java
new file mode 100644
index 0000000..8c7a5b0
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java
@@ -0,0 +1,66 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.springsecurity.facade.WrappedHttpServletRequest;
+import org.keycloak.adapters.springsecurity.facade.WrappedHttpServletResponse;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+import javax.security.cert.X509Certificate;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Simple {@link HttpFacade} wrapping an {@link HttpServletRequest} and {@link HttpServletResponse}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class SimpleHttpFacade implements HttpFacade {
+
+    private final HttpServletRequest request;
+    private final HttpServletResponse response;
+
+    /**
+     * Creates a new simple HTTP facade for the given request and response.
+     *
+     * @param request the current <code>HttpServletRequest</code> (required)
+     * @param response the current <code>HttpServletResponse</code> (required)
+     */
+    public SimpleHttpFacade(HttpServletRequest request, HttpServletResponse response) {
+        Assert.notNull(request, "HttpServletRequest required");
+        Assert.notNull(response, "HttpServletResponse required");
+        this.request = request;
+        this.response = response;
+    }
+
+    @Override
+    public KeycloakSecurityContext getSecurityContext() {
+
+        SecurityContext context = SecurityContextHolder.getContext();
+
+        if (context != null && context.getAuthentication() != null) {
+            return (KeycloakSecurityContext) context.getAuthentication().getDetails();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Request getRequest() {
+        return new WrappedHttpServletRequest(request);
+    }
+
+    @Override
+    public Response getResponse() {
+        return new WrappedHttpServletResponse(response);
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain() {
+        // TODO: implement me
+        return new X509Certificate[0];
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
new file mode 100644
index 0000000..478f182
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
@@ -0,0 +1,101 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.keycloak.adapters.HttpFacade.Cookie;
+import org.keycloak.adapters.HttpFacade.Request;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * Concrete Keycloak {@link Request request} implementation wrapping an {@link HttpServletRequest}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+class WrappedHttpServletRequest implements Request {
+
+    private final HttpServletRequest request;
+
+    /**
+     * Creates a new request for the given <code>HttpServletRequest</code>
+     *
+     * @param request the current <code>HttpServletRequest</code> (required)
+     */
+    public WrappedHttpServletRequest(HttpServletRequest request) {
+        Assert.notNull(request, "HttpServletRequest required");
+        this.request = request;
+    }
+
+    @Override
+    public String getMethod() {
+        return request.getMethod();
+    }
+
+    @Override
+    public String getURI() {
+        StringBuffer buf = request.getRequestURL();
+        if (request.getQueryString() != null) {
+            buf.append('?').append(request.getQueryString());
+        }
+        return buf.toString();
+    }
+
+    @Override
+    public boolean isSecure() {
+        return request.isSecure();
+    }
+
+    @Override
+    public String getQueryParamValue(String param) {
+        return request.getParameter(param);
+    }
+
+    @Override
+    public Cookie getCookie(String cookieName) {
+
+        for (javax.servlet.http.Cookie cookie : request.getCookies()) {
+            if (cookie.getName().equals(cookieName)) {
+                return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath());
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public String getHeader(String name) {
+        return request.getHeader(name);
+    }
+
+    @Override
+    public List<String> getHeaders(String name) {
+        Enumeration<String> values = request.getHeaders(name);
+        List<String> array = new ArrayList<String>();
+
+        while (values.hasMoreElements()) {
+            array.add(values.nextElement());
+        }
+
+        return Collections.unmodifiableList(array);
+    }
+
+    @Override
+    public InputStream getInputStream() {
+        try {
+            return request.getInputStream();
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to get request input stream", e);
+        }
+    }
+
+    @Override
+    public String getRemoteAddr() {
+        return request.getRemoteAddr();
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java
new file mode 100644
index 0000000..c0b5759
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java
@@ -0,0 +1,95 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.keycloak.adapters.HttpFacade.Response;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Concrete Keycloak {@link Response response} implementation wrapping an {@link HttpServletResponse}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+class WrappedHttpServletResponse implements Response {
+
+    private final HttpServletResponse response;
+
+    /**
+     * Creates a new response for the given <code>HttpServletResponse</code>.
+     *
+     * @param response the current <code>HttpServletResponse</code> (required)
+     */
+    public WrappedHttpServletResponse(HttpServletResponse response) {
+        this.response = response;
+    }
+
+    @Override
+    public void resetCookie(String name, String path) {
+        Cookie cookie = new Cookie(name, "");
+        cookie.setMaxAge(0);
+        if (path != null) {
+            cookie.setPath(path);
+        }
+        response.addCookie(cookie);
+    }
+
+    @Override
+    public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
+        Cookie cookie = new Cookie(name, value);
+
+        if (path != null) {
+            cookie.setPath(path);
+        }
+
+        if (domain != null) {
+            cookie.setDomain(domain);
+        }
+
+        cookie.setMaxAge(maxAge);
+        cookie.setSecure(secure);
+        cookie.setHttpOnly(httpOnly);
+
+        response.addCookie(cookie);
+    }
+
+    @Override
+    public void setStatus(int status) {
+        response.setStatus(status);
+    }
+
+    @Override
+    public void addHeader(String name, String value) {
+        response.addHeader(name, value);
+    }
+
+    @Override
+    public void setHeader(String name, String value) {
+        response.setHeader(name, value);
+    }
+
+    @Override
+    public OutputStream getOutputStream() {
+        try {
+            return response.getOutputStream();
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to return response output stream", e);
+        }
+    }
+
+    @Override
+    public void sendError(int code, String message) {
+        try {
+            response.sendError(code, message);
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to set HTTP status", e);
+        }
+    }
+
+    @Override
+    public void end() {
+        // TODO: do we need this?
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
new file mode 100644
index 0000000..55cbd92
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
@@ -0,0 +1,128 @@
+package org.keycloak.adapters.springsecurity.filter;
+
+import org.keycloak.adapters.AuthChallenge;
+import org.keycloak.adapters.AuthOutcome;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
+import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
+import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator;
+import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
+import org.keycloak.adapters.springsecurity.token.SpringSecurityTokenStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Provides a Keycloak authentication processing filter.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
+
+    /**
+     * Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
+     * and any request with a <code>Authorization</code> header.
+     */
+    public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
+            new OrRequestMatcher(new AntPathRequestMatcher("/sso/login"), new RequestHeaderRequestMatcher("Authorization"));
+
+    private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
+
+    private ApplicationContext applicationContext;
+    private AdapterDeploymentContextBean adapterDeploymentContextBean;
+    private AuthenticationManager authenticationManager;
+
+    /**
+     * Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and the
+     * {@link KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER default request matcher}.
+     *
+     * @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null)
+     * @see KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER
+     */
+    public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
+        this(authenticationManager, DEFAULT_REQUEST_MATCHER);
+    }
+
+    /**
+     * Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and
+     * {@link RequestMatcher}.
+     * <p>
+     *     Note: the given request matcher must support matching the <code>Authorization</code> header if
+     *     bearer token authentication is to be accepted.
+     * </p>
+     *
+     * @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null)
+     * @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to determine if authentication
+     *  is required (cannot be null)
+     *
+     *  @see RequestHeaderRequestMatcher
+     *  @see OrRequestMatcher
+     *
+     */
+    public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
+                requiresAuthenticationRequestMatcher) {
+        super(requiresAuthenticationRequestMatcher);
+        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+        this.authenticationManager = authenticationManager;
+        super.setAuthenticationManager(authenticationManager);
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        adapterDeploymentContextBean= applicationContext.getBean(AdapterDeploymentContextBean.class);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+            throws AuthenticationException, IOException, ServletException {
+
+        log.debug("Attempting Keycloak authentication");
+
+        KeycloakDeployment deployment = adapterDeploymentContextBean.getDeployment();
+        SimpleHttpFacade facade = new SimpleHttpFacade(request, response);
+        SpringSecurityTokenStore tokenStore = new SpringSecurityTokenStore(deployment, request);
+        SpringSecurityRequestAuthenticator authenticator
+                = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, -1);
+
+        AuthOutcome result = authenticator.authenticate();
+        AuthChallenge challenge = authenticator.getChallenge();
+
+        log.debug("Auth outcome: {}", result);
+
+        if (challenge != null) {
+            challenge.challenge(facade);
+        }
+
+        if (AuthOutcome.AUTHENTICATED.equals(result)) {
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
+            return authenticationManager.authenticate(authentication);
+        }
+
+        return null;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java
new file mode 100644
index 0000000..897010e
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java
@@ -0,0 +1,77 @@
+package org.keycloak.adapters.springsecurity.filter;
+
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.PreAuthActionsHandler;
+import org.keycloak.adapters.UserSessionManagement;
+import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
+import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Exposes a Keycloak adapter {@link PreAuthActionsHandler} as a Spring Security filter.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakPreAuthActionsFilter extends GenericFilterBean implements ApplicationContextAware {
+
+    private static final Logger log = LoggerFactory.getLogger(KeycloakPreAuthActionsFilter.class);
+
+    private ApplicationContext applicationContext;
+    private AdapterDeploymentContext deploymentContext;
+    private UserSessionManagement userSessionManagement;
+
+    public KeycloakPreAuthActionsFilter() {
+        super();
+    }
+
+    public KeycloakPreAuthActionsFilter(UserSessionManagement userSessionManagement) {
+        this.userSessionManagement = userSessionManagement;
+    }
+
+    @Override
+    protected void initFilterBean() throws ServletException {
+        AdapterDeploymentContextBean contextBean = applicationContext.getBean(AdapterDeploymentContextBean.class);
+        deploymentContext = contextBean.getDeploymentContext();
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        HttpFacade facade = new SimpleHttpFacade((HttpServletRequest)request, (HttpServletResponse)response);
+        PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade);
+        if (handler.handleRequest()) {
+            log.info("Pre-auth filter handled request: {}", ((HttpServletRequest) request).getRequestURI());
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    public void setUserSessionManagement(UserSessionManagement userSessionManagement) {
+        this.userSessionManagement = userSessionManagement;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java
new file mode 100644
index 0000000..3f73e8f
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides Spring Security filters for Keycloak.
+ */
+package org.keycloak.adapters.springsecurity.filter;
\ No newline at end of file
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java
new file mode 100644
index 0000000..5adf433
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakSecurityComponents.java
@@ -0,0 +1,10 @@
+package org.keycloak.adapters.springsecurity;
+
+/**
+ * Locator interface for Spring context component scanning.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public interface KeycloakSecurityComponents {
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java
new file mode 100644
index 0000000..3492e38
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/HttpSessionManager.java
@@ -0,0 +1,57 @@
+package org.keycloak.adapters.springsecurity.management;
+
+import org.keycloak.adapters.UserSessionManagement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+import java.util.List;
+
+/**
+ * User session manager for handling logout of Spring Secured sessions.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+@Component
+public class HttpSessionManager implements HttpSessionListener, UserSessionManagement {
+
+    private static final Logger log = LoggerFactory.getLogger(HttpSessionManager.class);
+    private SessionManagementStrategy sessions = new LocalSessionManagementStrategy();
+
+    @Override
+    public void sessionCreated(HttpSessionEvent event) {
+        log.debug("Session created: {}", event.getSession().getId());
+        HttpSession session = event.getSession();
+        sessions.store(session);
+    }
+
+    @Override
+    public void sessionDestroyed(HttpSessionEvent event) {
+        sessions.remove(event.getSession().getId());
+    }
+
+    @Override
+    public void logoutAll() {
+        log.info("Received request to log out all users.");
+        for (HttpSession session : sessions.getAll()) {
+            session.invalidate();
+        }
+        sessions.clear();
+    }
+
+    @Override
+    public void logoutHttpSessions(List<String> ids) {
+        log.info("Received request to log out {} session(s): {}", ids.size(), ids);
+        for (String id : ids) {
+            HttpSession session = sessions.remove(id);
+            if (session != null) {
+                session.invalidate();
+            }
+        }
+        sessions.clear();
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java
new file mode 100644
index 0000000..8676f1d
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/LocalSessionManagementStrategy.java
@@ -0,0 +1,34 @@
+package org.keycloak.adapters.springsecurity.management;
+
+import javax.servlet.http.HttpSession;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by scott on 4/24/15.
+ */
+public class LocalSessionManagementStrategy implements SessionManagementStrategy {
+
+    private final Map<String, HttpSession> sessions = new ConcurrentHashMap<String, HttpSession>();
+
+    @Override
+    public void clear() {
+        sessions.clear();
+    }
+
+    @Override
+    public Collection<HttpSession> getAll() {
+        return sessions.values();
+    }
+
+    @Override
+    public void store(HttpSession session) {
+        sessions.put(session.getId(), session);
+    }
+
+    @Override
+    public HttpSession remove(String id) {
+        return sessions.remove(id);
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java
new file mode 100644
index 0000000..96d6704
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/management/SessionManagementStrategy.java
@@ -0,0 +1,41 @@
+package org.keycloak.adapters.springsecurity.management;
+
+import javax.servlet.http.HttpSession;
+import java.util.Collection;
+
+/**
+ * Defines a session management strategy.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public interface SessionManagementStrategy {
+
+    /**
+     * Removes all sessions.
+     */
+    void clear();
+
+    /**
+     * Returns a collection containing all sessions.
+     *
+     * @return a <code>Collection</code> of all known <code>HttpSession</code>s, if any;
+     * an empty <code>Collection</code> otherwise
+     */
+    Collection<HttpSession> getAll();
+
+    /**
+     * Stores the given session.
+     *
+     * @param session the <code>HttpSession</code> to store (required)
+     */
+    void store(HttpSession session);
+
+    /**
+     * The unique identifier for the session to remove.
+     *
+     * @param id the unique identifier for the session to remove (required)
+     * @return the <code>HttpSession</code> if it exists; <code>null</code> otherwise
+     */
+    HttpSession remove(String id);
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java
new file mode 100644
index 0000000..4df6bc3
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides a Keycloak adapter for Spring Security.
+ */
+package org.keycloak.adapters.springsecurity;
\ No newline at end of file
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java
new file mode 100644
index 0000000..c6d341b
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/registration/NodeManager.java
@@ -0,0 +1,27 @@
+package org.keycloak.adapters.springsecurity.registration;
+
+import org.keycloak.adapters.KeycloakDeployment;
+
+/**
+ * Manages registration of application nodes.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public interface NodeManager {
+
+    /**
+     * Registers the given deployment with the Keycloak server.
+     *
+     * @param deployment the deployment to register (required)
+     */
+    void register(KeycloakDeployment deployment);
+
+    /**
+     * Unregisters the give deployment from the Keycloak server
+     * .
+     * @param deployment the deployment to unregister (required)
+     */
+    void unregister(KeycloakDeployment deployment);
+
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java
new file mode 100644
index 0000000..c96ae7e
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java
@@ -0,0 +1,55 @@
+package org.keycloak.adapters.springsecurity.token;
+
+import org.keycloak.adapters.KeycloakAccount;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
+
+import java.security.Principal;
+import java.util.Collection;
+
+/**
+ * Represents the token for a Keycloak authentication request or for an authenticated principal once the request has been
+ * processed by the {@link AuthenticationManager#authenticate(Authentication)}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakAuthenticationToken extends AbstractAuthenticationToken implements Authentication {
+
+    private Principal principal;
+
+    /**
+     * Creates a new, unauthenticated Keycloak security token for the given account.
+     */
+    public KeycloakAuthenticationToken(KeycloakAccount account) {
+        super(null);
+        Assert.notNull(account, "KeycloakAccount cannot be null");
+        Assert.notNull(account.getPrincipal(), "KeycloakAccount.getPrincipal() cannot be null");
+        this.principal = account.getPrincipal();
+        this.setDetails(account);
+    }
+
+    public KeycloakAuthenticationToken(KeycloakAccount account, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = account.getPrincipal();
+        this.setDetails(account);
+        setAuthenticated(true);
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.getAccount().getKeycloakSecurityContext();
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return principal;
+    }
+
+    public KeycloakAccount getAccount() {
+        return (KeycloakAccount) this.getDetails();
+    }
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java
new file mode 100644
index 0000000..3c6af11
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java
@@ -0,0 +1,122 @@
+package org.keycloak.adapters.springsecurity.token;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.AdapterTokenStore;
+import org.keycloak.adapters.KeycloakAccount;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.RequestAuthenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+/**
+ * Simple Spring {@link SecurityContext security context} aware {@link AdapterTokenStore adapter token store}.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @version $Revision: 1 $
+ */
+public class SpringSecurityTokenStore implements AdapterTokenStore {
+
+    private final Logger logger = LoggerFactory.getLogger(SpringSecurityTokenStore.class);
+
+    private final KeycloakDeployment deployment;
+    private final HttpServletRequest request;
+
+    public SpringSecurityTokenStore(KeycloakDeployment deployment, HttpServletRequest request) {
+        Assert.notNull(deployment, "KeycloakDeployment is required");
+        Assert.notNull(request, "HttpServletRequest is required");
+        this.deployment = deployment;
+        this.request = request;
+    }
+
+    @Override
+    public void checkCurrentToken() {
+        // no-op
+    }
+
+    @Override
+    public boolean isCached(RequestAuthenticator authenticator) {
+
+        logger.debug("Checking if {} is cached", authenticator);
+        SecurityContext context = SecurityContextHolder.getContext();
+        KeycloakAuthenticationToken token;
+        KeycloakSecurityContext keycloakSecurityContext;
+
+        if (context == null || context.getAuthentication() == null) {
+            return false;
+        }
+
+        if (KeycloakAuthenticationToken.class.isAssignableFrom(context.getAuthentication().getClass())) {
+            logger.warn("Expected a KeycloakAuthenticationToken, but found {}", context.getAuthentication());
+            return false;
+        }
+
+        logger.info("Remote logged in already. Establishing state from security context.");
+        token = (KeycloakAuthenticationToken) context.getAuthentication();
+        keycloakSecurityContext = token.getAccount().getKeycloakSecurityContext();
+
+        if (!deployment.getRealm().equals(keycloakSecurityContext.getRealm())) {
+            logger.info("Account from security context is from a different realm than for the request.");
+            logout();
+            return false;
+        }
+
+        if (keycloakSecurityContext.getToken().isExpired()) {
+            logger.warn("Security token expired ... not returning from cache");
+            return false;
+        }
+
+        request.setAttribute(KeycloakSecurityContext.class.getName(), keycloakSecurityContext);
+
+        return true;
+    }
+
+    @Override
+    public void saveAccountInfo(KeycloakAccount account) {
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (authentication != null) {
+            throw new IllegalStateException("Went to save Keycloak account {}, but already have {}");
+        }
+
+        logger.debug("Saving account info {}", account);
+        SecurityContextHolder.getContext().setAuthentication(new KeycloakAuthenticationToken(account));
+    }
+
+    @Override
+    public void logout() {
+
+        logger.debug("Handling logout request");
+        HttpSession session = request.getSession(false);
+
+        if (session != null) {
+            session.setAttribute(KeycloakSecurityContext.class.getName(), null);
+        }
+
+        SecurityContextHolder.clearContext();
+    }
+
+    @Override
+    public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
+        // no-op
+    }
+
+    @Override
+    public void saveRequest() {
+        // no-op, Spring Security will handle this
+    }
+
+    @Override
+    public boolean restoreRequest() {
+        // no-op, Spring Security will handle this
+        return false;
+    }
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java
new file mode 100644
index 0000000..b7204c3
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java
@@ -0,0 +1,51 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static org.junit.Assert.*;
+
+/**
+ * Keycloak authentication entry point tests.
+ */
+public class KeycloakAuthenticationEntryPointTest {
+
+    private KeycloakAuthenticationEntryPoint authenticationEntryPoint;
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+
+    @Before
+    public void setUp() throws Exception {
+        authenticationEntryPoint = new KeycloakAuthenticationEntryPoint();
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+    }
+
+    @Test
+    public void testCommence() throws Exception {
+        authenticationEntryPoint.commence(request, response, null);
+        assertEquals(HttpStatus.FOUND.value(), response.getStatus());
+        assertEquals(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location"));
+    }
+
+    @Test
+    public void testCommenceNotRootContext() throws Exception {
+        String contextPath = "/foo";
+        request.setContextPath(contextPath);
+        authenticationEntryPoint.commence(request, response, null);
+        assertEquals(HttpStatus.FOUND.value(), response.getStatus());
+        assertEquals(contextPath + KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location"));
+    }
+
+    @Test
+    public void testSetLoginUri() throws Exception {
+        final String logoutUri = "/foo";
+        authenticationEntryPoint.setLoginUri(logoutUri);
+        authenticationEntryPoint.commence(request, response, null);
+        assertEquals(HttpStatus.FOUND.value(), response.getStatus());
+        assertEquals(logoutUri, response.getHeader("Location"));
+    }
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java
new file mode 100644
index 0000000..a94ea11
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationProviderTest.java
@@ -0,0 +1,54 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.adapters.KeycloakAccount;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.mockito.internal.util.collections.Sets;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+
+import java.security.Principal;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Keycloak authentication provider tests.
+ */
+public class KeycloakAuthenticationProviderTest {
+
+    private KeycloakAuthenticationProvider provider = new KeycloakAuthenticationProvider();
+    private KeycloakAuthenticationToken token;
+    private Set<String> roles = Sets.newSet("user", "admin");
+
+    @Before
+    public void setUp() throws Exception {
+
+        Principal principal = mock(Principal.class);
+        RefreshableKeycloakSecurityContext securityContext = mock(RefreshableKeycloakSecurityContext.class);
+        KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
+
+        token = new KeycloakAuthenticationToken(account);
+    }
+
+    @Test
+    public void testAuthenticate() throws Exception {
+        Authentication result = provider.authenticate(token);
+        assertNotNull(result);
+        assertEquals(roles.size(), result.getAuthorities().size());
+        assertTrue(result.isAuthenticated());
+        assertNotNull(result.getPrincipal());
+        assertNotNull(result.getCredentials());
+        assertNotNull(result.getDetails());
+    }
+
+    @Test
+    public void testSupports() throws Exception {
+        assertTrue(provider.supports(KeycloakAuthenticationToken.class));
+        assertFalse(provider.supports(PreAuthenticatedAuthenticationToken.class));
+    }
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java
new file mode 100644
index 0000000..cdd305a
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticatorTest.java
@@ -0,0 +1,107 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.KeycloakPrincipal;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.AdapterTokenStore;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakAccount;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OAuthRequestAuthenticator;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.keycloak.representations.AccessToken;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.collections.Sets;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Spring Security request authenticator tests.
+ */
+public class SpringSecurityRequestAuthenticatorTest {
+
+    private SpringSecurityRequestAuthenticator authenticator;
+
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+
+    @Mock
+    private KeycloakDeployment deployment;
+
+    @Mock
+    private AdapterTokenStore tokenStore;
+
+    @Mock
+    private KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal;
+
+    @Mock
+    private AccessToken accessToken;
+
+    @Mock
+    private AccessToken.Access access;
+
+    @Mock
+    private RefreshableKeycloakSecurityContext refreshableKeycloakSecurityContext;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        request = spy(new MockHttpServletRequest());
+        response = new MockHttpServletResponse();
+        HttpFacade facade = new SimpleHttpFacade(request, response);
+
+        authenticator = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, 443);
+
+        // mocks
+        when(principal.getKeycloakSecurityContext()).thenReturn(refreshableKeycloakSecurityContext);
+
+        when(refreshableKeycloakSecurityContext.getDeployment()).thenReturn(deployment);
+        when(refreshableKeycloakSecurityContext.getToken()).thenReturn(accessToken);
+
+        when(accessToken.getRealmAccess()).thenReturn(access);
+        when(access.getRoles()).thenReturn(Sets.newSet("user", "admin"));
+
+        when(deployment.isUseResourceRoleMappings()).thenReturn(false);
+    }
+
+    @Test
+    public void testCreateOAuthAuthenticator() throws Exception {
+        OAuthRequestAuthenticator oathAuthenticator = authenticator.createOAuthAuthenticator();
+        assertNotNull(oathAuthenticator);
+    }
+
+    @Test
+    public void testCompleteOAuthAuthentication() throws Exception {
+        authenticator.completeOAuthAuthentication(principal);
+        verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext));
+        verify(tokenStore).saveAccountInfo(any(KeycloakAccount.class)); // FIXME: should verify account
+    }
+
+    @Test
+    public void testCompleteBearerAuthentication() throws Exception {
+        authenticator.completeBearerAuthentication(principal, "foo");
+        verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext));
+        assertNotNull(SecurityContextHolder.getContext().getAuthentication());
+        assertTrue(KeycloakAuthenticationToken.class.isAssignableFrom(SecurityContextHolder.getContext().getAuthentication().getClass()));
+    }
+
+    @Test
+    public void testGetHttpSessionIdTrue() throws Exception {
+        String sessionId = authenticator.getHttpSessionId(true);
+        assertNotNull(sessionId);
+    }
+
+    @Test
+    public void testGetHttpSessionIdFalse() throws Exception {
+        String sessionId = authenticator.getHttpSessionId(false);
+        assertNull(sessionId);
+    }
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java
new file mode 100644
index 0000000..91ec35b
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequestTest.java
@@ -0,0 +1,103 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import javax.servlet.http.Cookie;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * Wrapped HTTP servlet request tests.
+ */
+public class WrappedHttpServletRequestTest {
+
+    private static final String COOKIE_NAME = "oreo";
+    private static final String HEADER_MULTI_VALUE = "Multi";
+    private static final String HEADER_SINGLE_VALUE = "Single";
+    private static final String REQUEST_METHOD = RequestMethod.GET.name();
+    private static final String REQUEST_URI = "/foo/bar";
+    private static final String QUERY_PARM_1 = "code";
+    private static final String QUERY_PARM_2 = "code2";
+
+    private WrappedHttpServletRequest request;
+
+    @Before
+    public void setUp() throws Exception {
+        MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
+        request = new WrappedHttpServletRequest(mockHttpServletRequest);
+
+        mockHttpServletRequest.setMethod(REQUEST_METHOD);
+        mockHttpServletRequest.setRequestURI(REQUEST_URI);
+
+        mockHttpServletRequest.setSecure(true);
+        mockHttpServletRequest.setScheme("https");
+
+        mockHttpServletRequest.addHeader(HEADER_SINGLE_VALUE, "baz");
+        mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "foo");
+        mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "bar");
+
+        mockHttpServletRequest.addParameter(QUERY_PARM_1, "java");
+        mockHttpServletRequest.addParameter(QUERY_PARM_2, "groovy");
+        mockHttpServletRequest.setQueryString(String.format("%s=%s&%s=%s", QUERY_PARM_1, "java", QUERY_PARM_2, "groovy"));
+        mockHttpServletRequest.setCookies(new Cookie(COOKIE_NAME, "yum"));
+
+        mockHttpServletRequest.setContent("All work and no play makes Jack a dull boy".getBytes());
+    }
+
+    @Test
+    public void testGetMethod() throws Exception {
+        assertNotNull(request.getMethod());
+        assertEquals(REQUEST_METHOD, request.getMethod());
+    }
+
+    @Test
+    public void testGetURI() throws Exception {
+        assertEquals("https://localhost:80" + REQUEST_URI + "?code=java&code2=groovy" , request.getURI());
+    }
+
+    @Test
+    public void testIsSecure() throws Exception {
+        assertTrue(request.isSecure());
+    }
+
+    @Test
+    public void testGetQueryParamValue() throws Exception {
+        assertNotNull(request.getQueryParamValue(QUERY_PARM_1));
+        assertNotNull(request.getQueryParamValue(QUERY_PARM_2));
+    }
+
+    @Test
+    public void testGetCookie() throws Exception {
+        assertNotNull(request.getCookie(COOKIE_NAME));
+    }
+
+    @Test
+    public void testGetHeader() throws Exception {
+        String header = request.getHeader(HEADER_SINGLE_VALUE);
+        assertNotNull(header);
+        assertEquals("baz", header);
+    }
+
+    @Test
+    public void testGetHeaders() throws Exception {
+        List<String> headers = request.getHeaders(HEADER_MULTI_VALUE);
+        assertNotNull(headers);
+        assertEquals(2, headers.size());
+        assertTrue(headers.contains("foo"));
+        assertTrue(headers.contains("bar"));
+    }
+
+    @Test
+    public void testGetInputStream() throws Exception {
+        assertNotNull(request.getInputStream());
+    }
+
+    @Test
+    public void testGetRemoteAddr() throws Exception {
+        assertNotNull(request.getRemoteAddr());
+    }
+}
\ No newline at end of file
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java
new file mode 100644
index 0000000..408d9c5
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponseTest.java
@@ -0,0 +1,99 @@
+package org.keycloak.adapters.springsecurity.facade;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import javax.servlet.http.Cookie;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class WrappedHttpServletResponseTest {
+
+    private static final String COOKIE_DOMAIN = ".keycloak.org";
+    private static final String COOKIE_NAME = "foo";
+    private static final String COOKIE_PATH = "/bar";
+    private static final String COOKIE_VALUE = "onegreatcookie";
+    private static final String HEADER = "Test";
+
+    private WrappedHttpServletResponse response;
+    private MockHttpServletResponse mockResponse;
+
+    @Before
+    public void setUp() throws Exception {
+        mockResponse = spy(new MockHttpServletResponse());
+        response = new WrappedHttpServletResponse(mockResponse);
+    }
+
+    @Test
+    public void testResetCookie() throws Exception {
+        response.resetCookie(COOKIE_NAME, COOKIE_PATH);
+        verify(mockResponse).addCookie(any(Cookie.class));
+        assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName());
+        assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath());
+        assertEquals(0, mockResponse.getCookie(COOKIE_NAME).getMaxAge());
+        assertEquals("", mockResponse.getCookie(COOKIE_NAME).getValue());
+    }
+
+    @Test
+    public void testSetCookie() throws Exception {
+        int maxAge = 300;
+        response.setCookie(COOKIE_NAME, COOKIE_VALUE, COOKIE_PATH, COOKIE_DOMAIN, maxAge, false, true);
+        verify(mockResponse).addCookie(any(Cookie.class));
+        assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName());
+        assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath());
+        assertEquals(COOKIE_DOMAIN, mockResponse.getCookie(COOKIE_NAME).getDomain());
+        assertEquals(maxAge, mockResponse.getCookie(COOKIE_NAME).getMaxAge());
+        assertEquals(COOKIE_VALUE, mockResponse.getCookie(COOKIE_NAME).getValue());
+    }
+
+    @Test
+    public void testSetStatus() throws Exception {
+        int status = HttpStatus.OK.value();
+        response.setStatus(status);
+        verify(mockResponse).setStatus(eq(status));
+        assertEquals(status, mockResponse.getStatus());
+    }
+
+    @Test
+    public void testAddHeader() throws Exception {
+        String headerValue = "foo";
+        response.addHeader(HEADER, headerValue);
+        verify(mockResponse).addHeader(eq(HEADER), eq(headerValue));
+        assertTrue(mockResponse.containsHeader(HEADER));
+    }
+
+    @Test
+    public void testSetHeader() throws Exception {
+        String headerValue = "foo";
+        response.setHeader(HEADER, headerValue);
+        verify(mockResponse).setHeader(eq(HEADER), eq(headerValue));
+        assertTrue(mockResponse.containsHeader(HEADER));
+    }
+
+    @Test
+    public void testGetOutputStream() throws Exception {
+        assertNotNull(response.getOutputStream());
+        verify(mockResponse).getOutputStream();
+    }
+
+    @Test
+    public void testSendError() throws Exception {
+        int status = HttpStatus.UNAUTHORIZED.value();
+        String reason = HttpStatus.UNAUTHORIZED.getReasonPhrase();
+
+        response.sendError(status, reason);
+        verify(mockResponse).sendError(eq(status), eq(reason));
+        assertEquals(status, mockResponse.getStatus());
+        assertEquals(reason, mockResponse.getErrorMessage());
+    }
+
+    @Test
+    @Ignore
+    public void testEnd() throws Exception {
+        // TODO: what is an ended response, one that's committed?
+    }
+}
\ No newline at end of file