Merge pull request #1691 from patriot1burke/master

SAML SP Servlet Filter
This commit is contained in:
Bill Burke 2015-10-08 20:50:55 -04:00
commit 91f78c61b7
18 changed files with 1089 additions and 2 deletions

View file

@ -1,6 +1,7 @@
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
"http://www.docbook.org/xml/4.4/docbookx.dtd" "http://www.docbook.org/xml/4.4/docbookx.dtd"
[ [
<!ENTITY Overview SYSTEM "modules/overview.xml">
<!ENTITY AdapterConfig SYSTEM "modules/adapter-config.xml"> <!ENTITY AdapterConfig SYSTEM "modules/adapter-config.xml">
<!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml"> <!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml">
<!ENTITY TomcatAdapter SYSTEM "modules/tomcat-adapter.xml"> <!ENTITY TomcatAdapter SYSTEM "modules/tomcat-adapter.xml">
@ -13,7 +14,7 @@
<bookinfo> <bookinfo>
<title>Keycloak SAML Client Adapter Reference Guide</title> <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> <releaseinfo>&project.version;</releaseinfo>
</bookinfo> </bookinfo>
@ -39,6 +40,7 @@ This one is short
</programlisting> </programlisting>
</para> </para>
</preface> </preface>
&Overview;
&AdapterConfig; &AdapterConfig;
&JBossAdapter; &JBossAdapter;
&TomcatAdapter; &TomcatAdapter;

View file

@ -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>

View file

@ -17,6 +17,11 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
ConcurrentHashMap<String, Set<String>> principalToSession = new ConcurrentHashMap<>(); ConcurrentHashMap<String, Set<String>> principalToSession = new ConcurrentHashMap<>();
ConcurrentHashMap<String, String> sessionToPrincipal = new ConcurrentHashMap<>(); ConcurrentHashMap<String, String> sessionToPrincipal = new ConcurrentHashMap<>();
@Override
public boolean hasSession(String id) {
return sessionToSso.containsKey(id) || sessionToPrincipal.containsKey(id);
}
@Override @Override
public Set<String> getUserSessions(String principal) { public Set<String> getUserSessions(String principal) {
Set<String> lookup = principalToSession.get(principal); Set<String> lookup = principalToSession.get(principal);

View file

@ -7,6 +7,8 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface SessionIdMapper { public interface SessionIdMapper {
boolean hasSession(String id);
Set<String> getUserSessions(String principal); Set<String> getUserSessions(String principal);
String getSessionFromSSO(String sso); String getSessionFromSSO(String sso);

View file

@ -15,6 +15,7 @@
<modules> <modules>
<module>adapter-spi</module> <module>adapter-spi</module>
<module>servlet-adapter-spi</module>
<module>adapter-core</module> <module>adapter-core</module>
<module>jaxrs-oauth-client</module> <module>jaxrs-oauth-client</module>
<module>servlet-oauth-client</module> <module>servlet-oauth-client</module>

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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");
}
}

10
pom.xml
View file

@ -768,6 +768,11 @@
<artifactId>keycloak-adapter-spi</artifactId> <artifactId>keycloak-adapter-spi</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-adapter-spi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId> <artifactId>keycloak-adapter-core</artifactId>
@ -893,6 +898,11 @@
<artifactId>keycloak-tomcat6-adapter</artifactId> <artifactId>keycloak-tomcat6-adapter</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-server-filter-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-tomcat6-adapter</artifactId> <artifactId>keycloak-saml-tomcat6-adapter</artifactId>

View file

@ -88,6 +88,9 @@ public abstract class SamlAuthenticator {
protected AuthOutcome globalLogout() { protected AuthOutcome globalLogout() {
SamlSession account = sessionStore.getAccount(); SamlSession account = sessionStore.getAccount();
if (account == null) {
return AuthOutcome.NOT_ATTEMPTED;
}
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.assertionExpiration(30) .assertionExpiration(30)
.issuer(deployment.getEntityID()) .issuer(deployment.getEntityID())

View file

@ -20,5 +20,6 @@
<module>jetty</module> <module>jetty</module>
<module>wildfly</module> <module>wildfly</module>
<module>as7-eap6</module> <module>as7-eap6</module>
<module>servlet-filter</module>
</modules> </modules>
</project> </project>

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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() {
}
}

View file

@ -105,6 +105,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId> <artifactId>keycloak-saml-adapter-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-server-filter-adapter</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-undertow-adapter</artifactId> <artifactId>keycloak-saml-undertow-adapter</artifactId>

View file

@ -26,7 +26,11 @@ public class SendUsernameServlet extends HttpServlet {
if (checkRoles != null) { if (checkRoles != null) {
for (String role : checkRoles) { for (String role : checkRoles) {
System.out.println("check role: " + role); System.out.println("check role: " + role);
Assert.assertTrue(req.isUserInRole(role)); //Assert.assertTrue(req.isUserInRole(role));
if (!req.isUserInRole(role)) {
resp.sendError(403);
return;
}
} }
} }

View file

@ -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();
}
}

View file

@ -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";
}
}