keycloak-uncached
Changes
integration/pom.xml 1(+1 -0)
integration/servlet-adapter-spi/pom.xml 53(+53 -0)
integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java 98(+98 -0)
integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java 189(+189 -0)
pom.xml 10(+10 -0)
saml/client-adapter/pom.xml 1(+1 -0)
saml/client-adapter/servlet-filter/pom.xml 69(+69 -0)
saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java 218(+218 -0)
saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java 147(+147 -0)
testsuite/integration/pom.xml 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SendUsernameServlet.java 6(+5 -1)
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);
integration/pom.xml 1(+1 -0)
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>
integration/servlet-adapter-spi/pom.xml 53(+53 -0)
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())
saml/client-adapter/pom.xml 1(+1 -0)
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>
saml/client-adapter/servlet-filter/pom.xml 69(+69 -0)
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() {
+
+ }
+}
testsuite/integration/pom.xml 4(+4 -0)
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";
+ }
+
+
+}