keycloak-uncached

Merge pull request #1691 from patriot1burke/master SAML

10/8/2015 9:50:55 PM

Details

diff --git a/docbook/saml-adapter-docs/reference/en/en-US/master.xml b/docbook/saml-adapter-docs/reference/en/en-US/master.xml
index 55ce660..5c36b77 100755
--- a/docbook/saml-adapter-docs/reference/en/en-US/master.xml
+++ b/docbook/saml-adapter-docs/reference/en/en-US/master.xml
@@ -1,6 +1,7 @@
 <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
                          "http://www.docbook.org/xml/4.4/docbookx.dtd"
         [
+                <!ENTITY Overview SYSTEM "modules/overview.xml">
                 <!ENTITY AdapterConfig SYSTEM "modules/adapter-config.xml">
                 <!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml">
                 <!ENTITY TomcatAdapter SYSTEM "modules/tomcat-adapter.xml">
@@ -13,7 +14,7 @@
 
    <bookinfo>
       <title>Keycloak SAML Client Adapter Reference Guide</title>
-      <subtitle>SAML 2.0 Client Adapters for Java Applications</subtitle>
+      <subtitle>SAML 2.0 Client Adapters</subtitle>
       <releaseinfo>&project.version;</releaseinfo>
    </bookinfo>
 
@@ -39,6 +40,7 @@ This one is short
 </programlisting>
       </para>
    </preface>
+    &Overview;
     &AdapterConfig;
     &JBossAdapter;
     &TomcatAdapter;
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/overview.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/overview.xml
new file mode 100755
index 0000000..1616bbb
--- /dev/null
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/overview.xml
@@ -0,0 +1,9 @@
+<chapter>
+    <title>Overview</title>
+    <para>
+        This document describes the Keycloak SAML client adapter and how it can be configured for a variety of platforms.
+        The Keycloak SAML client adapter is a standalone component that provides generic SAML 2.0 support for your web applications.
+        There are no Keycloak server extensions built into it.  As long as the IDP you are talking to supports standard SAML, the
+        Keycloak SAML client adapter should be able to integrate with it.
+    </para>
+</chapter>
\ No newline at end of file
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java
index 4470c15..1be7159 100755
--- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java
@@ -18,6 +18,11 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
     ConcurrentHashMap<String, String> sessionToPrincipal = new ConcurrentHashMap<>();
 
     @Override
+    public boolean hasSession(String id) {
+        return sessionToSso.containsKey(id) || sessionToPrincipal.containsKey(id);
+    }
+
+    @Override
     public Set<String> getUserSessions(String principal) {
         Set<String> lookup = principalToSession.get(principal);
         if (lookup == null) return null;
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java
index 646f71b..d52e029 100755
--- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java
@@ -7,6 +7,8 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public interface SessionIdMapper {
+    boolean hasSession(String id);
+
     Set<String> getUserSessions(String principal);
 
     String getSessionFromSSO(String sso);
diff --git a/integration/pom.xml b/integration/pom.xml
index e4b7f27..c1d3346 100755
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -15,6 +15,7 @@
 
     <modules>
         <module>adapter-spi</module>
+        <module>servlet-adapter-spi</module>
         <module>adapter-core</module>
         <module>jaxrs-oauth-client</module>
         <module>servlet-oauth-client</module>
diff --git a/integration/servlet-adapter-spi/pom.xml b/integration/servlet-adapter-spi/pom.xml
new file mode 100755
index 0000000..b93ed63
--- /dev/null
+++ b/integration/servlet-adapter-spi/pom.xml
@@ -0,0 +1,53 @@
+<?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.6.0.Final-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-servlet-adapter-spi</artifactId>
+    <name>Keycloak Servlet Integration</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-spi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java
new file mode 100755
index 0000000..51c6909
--- /dev/null
+++ b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java
@@ -0,0 +1,98 @@
+package org.keycloak.adapters.servlet;
+
+import org.keycloak.adapters.AdapterSessionStore;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.util.MultivaluedHashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class FilterSessionStore implements AdapterSessionStore {
+    public static final String REDIRECT_URI = "__REDIRECT_URI";
+    public static final String SAVED_METHOD = "__SAVED_METHOD";
+    public static final String SAVED_HEADERS = "__SAVED_HEADERS";
+    public static final String SAVED_BODY = "__SAVED_BODY";
+    protected final HttpServletRequest request;
+    protected final HttpFacade facade;
+    protected final int maxBuffer;
+    protected byte[] restoredBuffer = null;
+
+    public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) {
+        this.request = request;
+        this.facade = facade;
+        this.maxBuffer = maxBuffer;
+    }
+
+    public void clearSavedRequest(HttpSession session) {
+        session.removeAttribute(REDIRECT_URI);
+        session.removeAttribute(SAVED_METHOD);
+        session.removeAttribute(SAVED_HEADERS);
+        session.removeAttribute(SAVED_BODY);
+    }
+
+    public String getRedirectUri() {
+        HttpSession session = request.getSession(true);
+        return (String)session.getAttribute(REDIRECT_URI);
+    }
+
+    @Override
+    public boolean restoreRequest() {
+        HttpSession session = request.getSession(false);
+        if (session == null) return false;
+        return session.getAttribute(REDIRECT_URI) != null;
+    }
+
+
+    @Override
+    public void saveRequest() {
+        HttpSession session = request.getSession(true);
+        session.setAttribute(REDIRECT_URI, facade.getRequest().getURI());
+        session.setAttribute(SAVED_METHOD, request.getMethod());
+        MultivaluedHashMap<String, String> headers = new MultivaluedHashMap<>();
+        Enumeration<String> names = request.getHeaderNames();
+        while (names.hasMoreElements()) {
+            String name = names.nextElement();
+            Enumeration<String> values = request.getHeaders(name);
+            while (values.hasMoreElements()) {
+                headers.add(name, values.nextElement());
+            }
+        }
+        session.setAttribute(SAVED_HEADERS, headers);
+        if (request.getMethod().equalsIgnoreCase("GET")) {
+            return;
+        }
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+        byte[] buffer = new byte[4096];
+        int bytesRead;
+        int totalRead = 0;
+        try {
+            InputStream is = request.getInputStream();
+
+            while ( (bytesRead = is.read(buffer) ) >= 0) {
+                os.write(buffer);
+                totalRead += bytesRead;
+                if (totalRead > maxBuffer) {
+                    throw new RuntimeException("max buffer reached on a saved request");
+                }
+
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        byte[] body = os.toByteArray();
+        // Only save the request body if there is something to save
+        if (body.length > 0) {
+            session.setAttribute(SAVED_BODY, body);
+        }
+    }
+
+}
diff --git a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
new file mode 100755
index 0000000..bc4eaa3
--- /dev/null
+++ b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
@@ -0,0 +1,189 @@
+package org.keycloak.adapters.servlet;
+
+import org.bouncycastle.ocsp.Req;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.util.KeycloakUriBuilder;
+import org.keycloak.util.MultivaluedHashMap;
+import org.keycloak.util.ServerCookie;
+import org.keycloak.util.UriUtils;
+
+import javax.security.cert.X509Certificate;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ServletHttpFacade implements HttpFacade {
+    protected final RequestFacade requestFacade = new RequestFacade();
+    protected final ResponseFacade responseFacade = new ResponseFacade();
+    protected HttpServletRequest request;
+    protected HttpServletResponse response;
+    protected MultivaluedHashMap<String, String> queryParameters;
+
+    public ServletHttpFacade(HttpServletRequest request, HttpServletResponse response) {
+        this.request = request;
+        this.response = response;
+    }
+
+    protected class RequestFacade implements 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 getFirstParam(String param) {
+            return request.getParameter(param);
+        }
+
+        @Override
+        public String getQueryParamValue(String param) {
+            if (queryParameters == null) {
+                queryParameters = UriUtils.decodeQueryString(request.getQueryString());
+            }
+            return queryParameters.getFirst(param);
+        }
+
+        @Override
+        public Cookie getCookie(String cookieName) {
+            if (request.getCookies() == null) return null;
+            javax.servlet.http.Cookie cookie = null;
+            for (javax.servlet.http.Cookie c : request.getCookies()) {
+                if (c.getName().equals(cookieName)) {
+                    cookie = c;
+                    break;
+                }
+            }
+            if (cookie == null) return null;
+            return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath());
+        }
+
+        @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> list = new LinkedList<>();
+            while (values.hasMoreElements()) list.add(values.nextElement());
+            return list;
+        }
+
+        @Override
+        public InputStream getInputStream() {
+            try {
+                return request.getInputStream();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public String getRemoteAddr() {
+            return request.getRemoteAddr();
+        }
+    }
+    public boolean isEnded() {
+        return responseFacade.isEnded();
+    }
+
+    protected class ResponseFacade implements Response {
+        protected boolean ended;
+
+        @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 void resetCookie(String name, String path) {
+            setCookie(name, "", path, null, 0, false, false);
+        }
+
+        @Override
+        public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
+            StringBuffer cookieBuf = new StringBuffer();
+            ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly);
+            String cookie = cookieBuf.toString();
+            response.addHeader("Set-Cookie", cookie);
+        }
+
+        @Override
+        public OutputStream getOutputStream() {
+            try {
+                return response.getOutputStream();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public void sendError(int code, String message) {
+            try {
+                response.sendError(code, message);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public void end() {
+            ended = true;
+        }
+
+        public boolean isEnded() {
+            return ended;
+        }
+    }
+
+
+    @Override
+    public Request getRequest() {
+        return requestFacade;
+    }
+
+    @Override
+    public Response getResponse() {
+        return responseFacade;
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain() {
+        throw new IllegalStateException("Not supported yet");
+    }
+}

pom.xml 10(+10 -0)

diff --git a/pom.xml b/pom.xml
index 224a323..61cfa08 100755
--- a/pom.xml
+++ b/pom.xml
@@ -770,6 +770,11 @@
             </dependency>
             <dependency>
                 <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-servlet-adapter-spi</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-adapter-core</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -895,6 +900,11 @@
             </dependency>
             <dependency>
                 <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-saml-server-filter-adapter</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-saml-tomcat6-adapter</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
index 953c4cf..b404f9b 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
@@ -88,6 +88,9 @@ public abstract class SamlAuthenticator {
 
     protected AuthOutcome globalLogout() {
         SamlSession account = sessionStore.getAccount();
+        if (account == null) {
+            return AuthOutcome.NOT_ATTEMPTED;
+        }
         SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
                 .assertionExpiration(30)
                 .issuer(deployment.getEntityID())
diff --git a/saml/client-adapter/pom.xml b/saml/client-adapter/pom.xml
index 36def45..1fa9028 100755
--- a/saml/client-adapter/pom.xml
+++ b/saml/client-adapter/pom.xml
@@ -20,5 +20,6 @@
         <module>jetty</module>
         <module>wildfly</module>
         <module>as7-eap6</module>
+        <module>servlet-filter</module>
     </modules>
 </project>
diff --git a/saml/client-adapter/servlet-filter/pom.xml b/saml/client-adapter/servlet-filter/pom.xml
new file mode 100755
index 0000000..219110c
--- /dev/null
+++ b/saml/client-adapter/servlet-filter/pom.xml
@@ -0,0 +1,69 @@
+<?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.6.0.Final-SNAPSHOT</version>
+		<relativePath>../../../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-saml-server-filter-adapter</artifactId>
+	<name>Keycloak SAML Servlet Filter</name>
+	<description />
+
+	<dependencies>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+        </dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-common</artifactId>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-spi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-servlet-adapter-spi</artifactId>
+        </dependency>
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcprov-jdk15on</artifactId>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-saml-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-saml-adapter-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<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/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java
new file mode 100755
index 0000000..dc4cd32
--- /dev/null
+++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java
@@ -0,0 +1,218 @@
+package org.keycloak.adapters.saml.servlet;
+
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.SessionIdMapper;
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.saml.SamlSessionStore;
+import org.keycloak.adapters.servlet.FilterSessionStore;
+import org.keycloak.util.MultivaluedHashMap;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpSession;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class FilterSamlSessionStore extends FilterSessionStore implements SamlSessionStore {
+    protected static Logger log = Logger.getLogger(SamlSessionStore.class);
+    protected final SessionIdMapper idMapper;
+
+    public FilterSamlSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, SessionIdMapper idMapper) {
+        super(request, facade, maxBuffer);
+        this.idMapper = idMapper;
+    }
+
+    protected boolean needRequestRestore;
+
+    @Override
+    public void logoutAccount() {
+        HttpSession session = request.getSession(false);
+        if (session == null) return;
+        if (session != null) {
+            if (idMapper != null) idMapper.removeSession(session.getId());
+            SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
+            if (samlSession != null) {
+                session.removeAttribute(SamlSession.class.getName());
+            }
+            clearSavedRequest(session);
+        }
+     }
+
+    @Override
+    public void logoutByPrincipal(String principal) {
+        SamlSession account = getAccount();
+        if (account != null && account.getPrincipal().getSamlSubject().equals(principal)) {
+            logoutAccount();
+        }
+        if (idMapper != null) {
+            Set<String> sessions = idMapper.getUserSessions(principal);
+            if (sessions != null) {
+                List<String> ids = new LinkedList<String>();
+                ids.addAll(sessions);
+                for (String id : ids) {
+                    idMapper.removeSession(id);
+                }
+            }
+        }
+
+    }
+
+    @Override
+    public void logoutBySsoId(List<String> ssoIds) {
+        SamlSession account = getAccount();
+        for (String ssoId : ssoIds) {
+            if (account != null && account.getSessionIndex().equals(ssoId)) {
+                logoutAccount();
+            } else if (idMapper != null) {
+                String sessionId = idMapper.getSessionFromSSO(ssoId);
+                idMapper.removeSession(sessionId);
+            }
+        }
+    }
+
+    @Override
+    public boolean isLoggedIn() {
+        HttpSession session = request.getSession(false);
+        if (session == null) return false;
+        if (session == null) {
+            log.debug("session was null, returning null");
+            return false;
+        }
+        final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
+        if (samlSession == null) {
+            log.debug("SamlSession was not in session, returning null");
+            return false;
+        }
+        if (idMapper != null && !idMapper.hasSession(session.getId())) {
+            logoutAccount();
+            return false;
+        }
+
+        needRequestRestore = restoreRequest();
+        return true;
+    }
+
+    public HttpServletRequestWrapper getWrap() {
+        HttpSession session = request.getSession(true);
+        final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
+        if (needRequestRestore) {
+            final String method = (String)session.getAttribute(SAVED_METHOD);
+            final byte[] body = (byte[])session.getAttribute(SAVED_BODY);
+            final MultivaluedHashMap<String, String> headers = (MultivaluedHashMap<String, String>)session.getAttribute(SAVED_HEADERS);
+            clearSavedRequest(session);
+            HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
+                @Override
+                public boolean isUserInRole(String role) {
+                    return samlSession.getRoles().contains(role);
+                }
+
+                @Override
+                public Principal getUserPrincipal() {
+                    return samlSession.getPrincipal();
+                }
+
+                @Override
+                public String getMethod() {
+                    if (needRequestRestore) {
+                        return method;
+                    } else {
+                        return super.getMethod();
+
+                    }
+                }
+
+                @Override
+                public String getHeader(String name) {
+                    if (needRequestRestore && headers != null) {
+                        return headers.getFirst(name);
+                    }
+                    return super.getHeader(name);
+                }
+
+                @Override
+                public Enumeration<String> getHeaders(String name) {
+                    if (needRequestRestore && headers != null) {
+                        List<String> values = headers.getList(name);
+                        if (values == null) return Collections.emptyEnumeration();
+                        else return Collections.enumeration(values);
+                    }
+                    return super.getHeaders(name);
+                }
+
+                @Override
+                public Enumeration<String> getHeaderNames() {
+                    if (needRequestRestore && headers != null) {
+                        return Collections.enumeration(headers.keySet());
+                    }
+                    return super.getHeaderNames();
+                }
+
+                @Override
+                public ServletInputStream getInputStream() throws IOException {
+
+                    if (needRequestRestore && body != null) {
+                        final ByteArrayInputStream is = new ByteArrayInputStream(body);
+                        return new ServletInputStream() {
+                            @Override
+                            public int read() throws IOException {
+                                return is.read();
+                            }
+                        };
+                    }
+                    return super.getInputStream();
+                }
+            };
+            return wrapper;
+        } else {
+            return new HttpServletRequestWrapper(request) {
+                @Override
+                public boolean isUserInRole(String role) {
+                    return samlSession.getRoles().contains(role);
+                }
+
+                @Override
+                public Principal getUserPrincipal() {
+                    return samlSession.getPrincipal();
+                }
+
+            };
+        }
+
+
+
+    }
+
+    @Override
+    public void saveAccount(SamlSession account) {
+        HttpSession session = request.getSession(true);
+        session.setAttribute(SamlSession.class.getName(), account);
+        if (idMapper != null) idMapper.map(account.getSessionIndex(),  account.getPrincipal().getSamlSubject(), session.getId());
+    }
+
+    @Override
+    public SamlSession getAccount() {
+        HttpSession session = request.getSession(false);
+        if (session == null) return null;
+        return (SamlSession)session.getAttribute(SamlSession.class.getName());
+    }
+
+    @Override
+    public String getRedirectUri() {
+        HttpSession session = request.getSession(false);
+        if (session == null) return null;
+        return (String)session.getAttribute(REDIRECT_URI);
+    }
+
+}
diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java
new file mode 100755
index 0000000..c1b6ad5
--- /dev/null
+++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java
@@ -0,0 +1,147 @@
+package org.keycloak.adapters.saml.servlet;
+
+import org.keycloak.adapters.AuthChallenge;
+import org.keycloak.adapters.AuthOutcome;
+import org.keycloak.adapters.InMemorySessionIdMapper;
+import org.keycloak.adapters.SessionIdMapper;
+import org.keycloak.adapters.saml.DefaultSamlDeployment;
+import org.keycloak.adapters.saml.SamlAuthenticator;
+import org.keycloak.adapters.saml.SamlDeployment;
+import org.keycloak.adapters.saml.SamlDeploymentContext;
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
+import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
+import org.keycloak.adapters.servlet.ServletHttpFacade;
+import org.keycloak.saml.common.exceptions.ParsingException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SamlFilter implements Filter {
+    protected SamlDeploymentContext deploymentContext;
+    protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
+    private final static Logger log = Logger.getLogger(""+SamlFilter.class);
+
+    @Override
+    public void init(final FilterConfig filterConfig) throws ServletException {
+        String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
+        if (configResolverClass != null) {
+            try {
+                throw new RuntimeException("Not implemented yet");
+                //KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
+                //deploymentContext = new SamlDeploymentContext(configResolver);
+                //log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
+            } catch (Exception ex) {
+                log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
+                //deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
+            }
+        } else {
+            String path = "/WEB-INF/keycloak-saml.xml";
+            String pathParam = filterConfig.getInitParameter("keycloak.config.file");
+            if (pathParam != null) path = pathParam;
+            InputStream is = filterConfig.getServletContext().getResourceAsStream(path);
+            final SamlDeployment deployment;
+            if (is == null) {
+                log.info("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
+                deployment = new DefaultSamlDeployment();
+            } else {
+                try {
+                    ResourceLoader loader = new ResourceLoader() {
+                        @Override
+                        public InputStream getResourceAsStream(String resource) {
+                            return filterConfig.getServletContext().getResourceAsStream(resource);
+                        }
+                    };
+                    deployment = new DeploymentBuilder().build(is, loader);
+                } catch (ParsingException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            deploymentContext = new SamlDeploymentContext(deployment);
+            log.fine("Keycloak is using a per-deployment configuration.");
+        }
+        filterConfig.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext);
+
+    }
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+        ServletHttpFacade facade = new ServletHttpFacade(request, response);
+        SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
+        if (deployment == null || !deployment.isConfigured()) {
+            response.sendError(403);
+            log.fine("deployment not configured");
+            return;
+        }
+        FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
+
+
+        SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
+            @Override
+            protected void completeAuthentication(SamlSession account) {
+
+            }
+        };
+        AuthOutcome outcome = authenticator.authenticate();
+        if (outcome == AuthOutcome.AUTHENTICATED) {
+            log.fine("AUTHENTICATED");
+            if (facade.isEnded()) {
+                return;
+            }
+            HttpServletRequestWrapper wrapper = tokenStore.getWrap();
+            chain.doFilter(wrapper, res);
+            return;
+        }
+        if (outcome == AuthOutcome.LOGGED_OUT) {
+            tokenStore.logoutAccount();
+            if (deployment.getLogoutPage() != null) {
+                RequestDispatcher disp = req.getRequestDispatcher(deployment.getLogoutPage());
+                disp.forward(req, res);
+                return;
+            }
+            chain.doFilter(req, res);
+            return;
+        }
+
+        AuthChallenge challenge = authenticator.getChallenge();
+        if (challenge != null) {
+            log.fine("challenge");
+            if (challenge.errorPage()) {
+                response.sendError(403);
+                return;
+            }
+            log.fine("sending challenge");
+            challenge.challenge(facade);
+        }
+        if (outcome == AuthOutcome.FAILED) {
+            response.sendError(403);
+        } else if (!facade.isEnded()) {
+            chain.doFilter(req, res);
+            return;
+        }
+
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 979d65e..7587247 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -107,6 +107,10 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-saml-server-filter-adapter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-saml-undertow-adapter</artifactId>
         </dependency>
         <dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SendUsernameServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SendUsernameServlet.java
index faa5014..f3a14f4 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SendUsernameServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SendUsernameServlet.java
@@ -26,7 +26,11 @@ public class SendUsernameServlet extends HttpServlet {
         if (checkRoles != null) {
             for (String role : checkRoles) {
                 System.out.println("check role: " + role);
-                Assert.assertTrue(req.isUserInRole(role));
+                //Assert.assertTrue(req.isUserInRole(role));
+                if (!req.isUserInRole(role)) {
+                    resp.sendError(403);
+                    return;
+                }
             }
 
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java
new file mode 100755
index 0000000..88f97a6
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java
@@ -0,0 +1,147 @@
+package org.keycloak.testsuite.samlfilter;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
+import org.keycloak.testsuite.keycloaksaml.SamlSPFacade;
+import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
+import org.openqa.selenium.WebDriver;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SamlAdapterTest {
+
+    @ClassRule
+    public static SamlKeycloakRule keycloakRule = new SamlKeycloakRule() {
+        @Override
+        public void initWars() {
+             ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
+
+            initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post",  "post.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig",  "post-sig.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email",  "post-sig-email.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient",  "post-sig-transient.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-post-persistent", "/sales-post-sig-persistent",  "post-sig-persistent.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-metadata", "/sales-metadata",  "post-metadata.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-get", "/employee-sig",  "employee-sig.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/mappers", "/employee2",  "employee2.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/signed-front-get", "/employee-sig-front",  "employee-sig-front.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/bad-client-signed-post", "/bad-client-sales-post-sig",  "bad-client-post-sig.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig",  "bad-realm-post-sig.war", classLoader);
+            initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
+            SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
+
+
+
+        }
+
+        @Override
+        public String getRealmJson() {
+            return "/keycloak-saml/testsaml.json";
+        }
+    };
+
+    @Rule
+    public SamlAdapterTestStrategy testStrategy = new SamlAdapterTestStrategy("http://localhost:8081/auth", "http://localhost:8081", keycloakRule);
+
+    @Test
+    public void testPostBadRealmSignature() {
+        testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
+            @Override
+            public void check(WebDriver driver) {
+                Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
+            }
+        });
+    }
+
+    @Test
+    public void testPostSimpleUnauthorized() {
+        List<String> requiredRoles = new LinkedList<>();
+        requiredRoles.add("manager");
+        requiredRoles.add("employee");
+        requiredRoles.add("user");
+        SendUsernameServlet.checkRoles = requiredRoles;
+        try {
+            testStrategy.testPostSimpleUnauthorized(new SamlAdapterTestStrategy.CheckAuthError() {
+                @Override
+                public void check(WebDriver driver) {
+                    Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
+                }
+            });
+        } finally {
+            SendUsernameServlet.checkRoles = null;
+        }
+    }
+
+    @Test
+    public void testMetadataPostSignedLoginLogout() throws Exception {
+        testStrategy.testMetadataPostSignedLoginLogout();
+    }
+
+    @Test
+    public void testRedirectSignedLoginLogout() {
+        testStrategy.testRedirectSignedLoginLogout();
+    }
+
+    @Test
+    public void testPostSignedLoginLogoutEmailNameID() {
+        testStrategy.testPostSignedLoginLogoutEmailNameID();
+    }
+
+    @Test
+    public void testPostEncryptedLoginLogout() {
+        testStrategy.testPostEncryptedLoginLogout();
+    }
+
+    @Test
+    public void testRedirectSignedLoginLogoutFrontNoSSO() {
+        testStrategy.testRedirectSignedLoginLogoutFrontNoSSO();
+    }
+
+    @Test
+    public void testPostSimpleLoginLogout() {
+        testStrategy.testPostSimpleLoginLogout();
+    }
+
+    @Test
+    public void testPostSignedLoginLogoutTransientNameID() {
+        testStrategy.testPostSignedLoginLogoutTransientNameID();
+    }
+
+    @Test
+    public void testPostSimpleLoginLogoutIdpInitiated() {
+        testStrategy.testPostSimpleLoginLogoutIdpInitiated();
+    }
+
+    @Test
+    public void testAttributes() throws Exception {
+        testStrategy.testAttributes();
+    }
+
+    @Test
+    public void testPostSignedLoginLogoutPersistentNameID() {
+        testStrategy.testPostSignedLoginLogoutPersistentNameID();
+    }
+
+    @Test
+    public void testPostBadClientSignature() {
+        testStrategy.testPostBadClientSignature();
+    }
+
+    @Test
+    public void testRedirectSignedLoginLogoutFront() {
+        testStrategy.testRedirectSignedLoginLogoutFront();
+    }
+
+    @Test
+    public void testPostSignedLoginLogout() {
+        testStrategy.testPostSignedLoginLogout();
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java
new file mode 100755
index 0000000..e1fd3c8
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java
@@ -0,0 +1,125 @@
+package org.keycloak.testsuite.samlfilter;
+
+import io.undertow.security.idm.Account;
+import io.undertow.security.idm.Credential;
+import io.undertow.security.idm.IdentityManager;
+import io.undertow.server.handlers.resource.Resource;
+import io.undertow.server.handlers.resource.ResourceChangeListener;
+import io.undertow.server.handlers.resource.ResourceManager;
+import io.undertow.server.handlers.resource.URLResource;
+import io.undertow.servlet.api.DeploymentInfo;
+import io.undertow.servlet.api.FilterInfo;
+import io.undertow.servlet.api.LoginConfig;
+import io.undertow.servlet.api.SecurityConstraint;
+import io.undertow.servlet.api.ServletInfo;
+import io.undertow.servlet.api.WebResourceCollection;
+import org.keycloak.adapters.saml.servlet.SamlFilter;
+import org.keycloak.adapters.saml.undertow.SamlServletExtension;
+import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
+import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+
+import javax.servlet.DispatcherType;
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
+
+    public static class TestResourceManager implements ResourceManager {
+
+        private final String basePath;
+
+        public TestResourceManager(String basePath){
+            this.basePath = basePath;
+        }
+
+        @Override
+        public Resource getResource(String path) throws IOException {
+            String temp = path;
+            String fullPath = basePath + temp;
+            URL url = getClass().getResource(fullPath);
+            if (url == null) {
+                System.out.println("url is null: " + fullPath);
+            }
+            return new URLResource(url, url.openConnection(), path);
+        }
+
+        @Override
+        public boolean isResourceChangeListenerSupported() {
+            throw new RuntimeException();
+        }
+
+        @Override
+        public void registerResourceChangeListener(ResourceChangeListener listener) {
+            throw new RuntimeException();
+        }
+
+        @Override
+        public void removeResourceChangeListener(ResourceChangeListener listener) {
+            throw new RuntimeException();
+        }
+
+        @Override
+        public void close() throws IOException {
+            throw new RuntimeException();
+        }
+    }
+
+    public static class TestIdentityManager implements IdentityManager {
+        @Override
+        public Account verify(Account account) {
+            return account;
+        }
+
+        @Override
+        public Account verify(String userName, Credential credential) {
+            throw new RuntimeException("WTF");
+        }
+
+        @Override
+        public Account verify(Credential credential) {
+            throw new RuntimeException();
+        }
+    }
+
+    @Override
+    protected void setupKeycloak() {
+        String realmJson = getRealmJson();
+        server.importRealm(getClass().getResourceAsStream(realmJson));
+        initWars();
+    }
+
+    public abstract void initWars();
+
+    public void initializeSamlSecuredWar(String warResourcePath, String contextPath, String warDeploymentName, ClassLoader classLoader) {
+
+        ServletInfo regularServletInfo = new ServletInfo("servlet", SendUsernameServlet.class)
+                .addMapping("/*");
+
+        FilterInfo samlFilter = new FilterInfo("saml-filter", SamlFilter.class);
+
+
+        ResourceManager resourceManager = new TestResourceManager(warResourcePath);
+
+        DeploymentInfo deploymentInfo = new DeploymentInfo()
+                .setClassLoader(classLoader)
+                .setIdentityManager(new TestIdentityManager())
+                .setContextPath(contextPath)
+                .setDeploymentName(warDeploymentName)
+                .setResourceManager(resourceManager)
+                .addServlets(regularServletInfo)
+                .addFilter(samlFilter)
+                .addFilterUrlMapping("saml-filter", "/*", DispatcherType.REQUEST)
+                .addServletExtension(new SamlServletExtension());
+        server.getServer().deploy(deploymentInfo);
+    }
+
+    public String getRealmJson() {
+        return "/keycloak-saml/testsaml.json";
+    }
+
+
+}