saml and oidc filters

This commit is contained in:
Bill Burke 2015-10-09 18:07:50 -04:00
parent d39aee0a72
commit 9ced56d8d7
26 changed files with 1159 additions and 114 deletions

View file

@ -114,6 +114,11 @@ public class BearerTokenRequestAuthenticator {
return false;
}
@Override
public int getResponseCode() {
return 0;
}
@Override
public boolean challenge(HttpFacade exchange) {
// do the same thing as client cert auth
@ -139,6 +144,11 @@ public class BearerTokenRequestAuthenticator {
return true;
}
@Override
public int getResponseCode() {
return 401;
}
@Override
public boolean challenge(HttpFacade facade) {
facade.getResponse().setStatus(401);

View file

@ -181,6 +181,11 @@ public class OAuthRequestAuthenticator {
public boolean errorPage() {
return true;
}
@Override
public int getResponseCode() {
return 403;
}
};
}
return new AuthChallenge() {
@ -190,6 +195,11 @@ public class OAuthRequestAuthenticator {
return false;
}
@Override
public int getResponseCode() {
return 0;
}
@Override
public boolean challenge(HttpFacade exchange) {
tokenStore.saveRequest();
@ -262,6 +272,11 @@ public class OAuthRequestAuthenticator {
return true;
}
@Override
public int getResponseCode() {
return code;
}
@Override
public boolean challenge(HttpFacade exchange) {
exchange.getResponse().setStatus(code);

View file

@ -13,9 +13,17 @@ public interface AuthChallenge {
boolean challenge(HttpFacade exchange);
/**
* Whether or not an error page should be displayed if possible
* Whether or not an error page should be displayed if possible along with the challenge
*
* @return
*/
boolean errorPage();
/**
* If errorPage is true, this is the response code the challenge will send. This is used by platforms
* that call HttpServletResponse.sendError() to forward to error page.
*
* @return
*/
int getResponseCode();
}

View file

@ -22,6 +22,14 @@ public class InMemorySessionIdMapper implements SessionIdMapper {
return sessionToSso.containsKey(id) || sessionToPrincipal.containsKey(id);
}
@Override
public void clear() {
ssoToSession.clear();
sessionToSso.clear();
principalToSession.clear();
sessionToPrincipal.clear();
}
@Override
public Set<String> getUserSessions(String principal) {
Set<String> lookup = principalToSession.get(principal);

View file

@ -9,6 +9,8 @@ import java.util.Set;
public interface SessionIdMapper {
boolean hasSession(String id);
void clear();
Set<String> getUserSessions(String principal);
String getSessionFromSSO(String sso);

View file

@ -262,6 +262,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
if (challenge.errorPage() && errorPage != null) {
Response response = (Response)res;
try {
@ -271,7 +272,6 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
}
}
challenge.challenge(facade);
}
return Authentication.SEND_CONTINUE;
}

View file

@ -26,6 +26,7 @@
<module>undertow-adapter-spi</module>
<module>undertow</module>
<module>wildfly</module>
<module>servlet-filter</module>
<module>js</module>
<module>installed</module>
<module>admin-client</module>

View file

@ -2,14 +2,32 @@ package org.keycloak.adapters.servlet;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.util.Encode;
import org.keycloak.util.MultivaluedHashMap;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -24,6 +42,7 @@ public class FilterSessionStore implements AdapterSessionStore {
protected final HttpFacade facade;
protected final int maxBuffer;
protected byte[] restoredBuffer = null;
protected boolean needRequestRestore;
public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) {
this.request = request;
@ -38,6 +57,249 @@ public class FilterSessionStore implements AdapterSessionStore {
session.removeAttribute(SAVED_BODY);
}
public void servletRequestLogout() {
}
public static String getCharsetFromContentType(String contentType) {
if (contentType == null)
return (null);
int start = contentType.indexOf("charset=");
if (start < 0)
return (null);
String encoding = contentType.substring(start + 8);
int end = encoding.indexOf(';');
if (end >= 0)
encoding = encoding.substring(0, end);
encoding = encoding.trim();
if ((encoding.length() > 2) && (encoding.startsWith("\""))
&& (encoding.endsWith("\"")))
encoding = encoding.substring(1, encoding.length() - 1);
return (encoding.trim());
}
public HttpServletRequestWrapper buildWrapper(HttpSession session, final KeycloakAccount account) {
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) {
protected MultivaluedHashMap<String, String> parameters;
MultivaluedHashMap<String, String> getParams() {
if (parameters != null) return parameters;
String contentType = getContentType();
contentType = contentType.toLowerCase();
if (contentType.startsWith("application/x-www-form-urlencoded")) {
ByteArrayInputStream is = new ByteArrayInputStream(body);
try {
parameters = parseForm(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return parameters;
}
@Override
public boolean isUserInRole(String role) {
return account.getRoles().contains(role);
}
@Override
public Principal getUserPrincipal() {
return account.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.toLowerCase());
}
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
if (needRequestRestore && headers != null) {
List<String> values = headers.getList(name.toLowerCase());
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();
}
@Override
public void logout() throws ServletException {
servletRequestLogout();
}
@Override
public long getDateHeader(String name) {
if (!needRequestRestore) return super.getDateHeader(name);
throw new RuntimeException("This method is not supported in a restored authenticated request");
}
@Override
public int getIntHeader(String name) {
if (!needRequestRestore) return super.getIntHeader(name);
String value = getHeader(name);
if (value == null) return -1;
return Integer.valueOf(value);
}
@Override
public String[] getParameterValues(String name) {
if (!needRequestRestore) return super.getParameterValues(name);
MultivaluedHashMap<String, String> formParams = getParams();
if (formParams == null) {
return super.getParameterValues(name);
}
String[] values = request.getParameterValues(name);
List<String> list = new LinkedList<>();
if (values != null) {
for (String val : values) list.add(val);
}
List<String> vals = formParams.get(name);
if (vals != null) list.addAll(vals);
return list.toArray(new String[list.size()]);
}
@Override
public Enumeration<String> getParameterNames() {
if (!needRequestRestore) return super.getParameterNames();
MultivaluedHashMap<String, String> formParams = getParams();
if (formParams == null) {
return super.getParameterNames();
}
Set<String> names = new HashSet<>();
Enumeration<String> qnames = super.getParameterNames();
while (qnames.hasMoreElements()) names.add(qnames.nextElement());
names.addAll(formParams.keySet());
return Collections.enumeration(names);
}
@Override
public Map<String, String[]> getParameterMap() {
if (!needRequestRestore) return super.getParameterMap();
MultivaluedHashMap<String, String> formParams = getParams();
if (formParams == null) {
return super.getParameterMap();
}
Map<String, String[]> map = new HashMap<>();
Enumeration<String> names = getParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
String[] values = getParameterValues(name);
if (values != null) {
map.put(name, values);
}
}
return map;
}
@Override
public String getParameter(String name) {
if (!needRequestRestore) return super.getParameter(name);
String param = super.getParameter(name);
if (param != null) return param;
MultivaluedHashMap<String, String> formParams = getParams();
if (formParams == null) {
return null;
}
return formParams.getFirst(name);
}
@Override
public BufferedReader getReader() throws IOException {
if (!needRequestRestore) return super.getReader();
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public int getContentLength() {
if (!needRequestRestore) return super.getContentLength();
String header = getHeader("content-length");
if (header == null) return -1;
return Integer.valueOf(header);
}
@Override
public String getContentType() {
if (!needRequestRestore) return super.getContentType();
return getHeader("content-type");
}
@Override
public String getCharacterEncoding() {
if (!needRequestRestore) return super.getCharacterEncoding();
return getCharsetFromContentType(getContentType());
}
};
return wrapper;
} else {
return new HttpServletRequestWrapper(request) {
@Override
public boolean isUserInRole(String role) {
return account.getRoles().contains(role);
}
@Override
public Principal getUserPrincipal() {
return account.getPrincipal();
}
@Override
public void logout() throws ServletException {
servletRequestLogout();
}
};
}
}
public String getRedirectUri() {
HttpSession session = request.getSession(true);
return (String)session.getAttribute(REDIRECT_URI);
@ -50,6 +312,44 @@ public class FilterSessionStore implements AdapterSessionStore {
return session.getAttribute(REDIRECT_URI) != null;
}
public static MultivaluedHashMap<String, String> parseForm(InputStream entityStream)
throws IOException
{
char[] buffer = new char[100];
StringBuffer buf = new StringBuffer();
BufferedReader reader = new BufferedReader(new InputStreamReader(entityStream));
int wasRead = 0;
do
{
wasRead = reader.read(buffer, 0, 100);
if (wasRead > 0) buf.append(buffer, 0, wasRead);
} while (wasRead > -1);
String form = buf.toString();
MultivaluedHashMap<String, String> formData = new MultivaluedHashMap<String, String>();
if ("".equals(form)) return formData;
String[] params = form.split("&");
for (String param : params)
{
if (param.indexOf('=') >= 0)
{
String[] nv = param.split("=");
String val = nv.length > 1 ? nv[1] : "";
formData.add(Encode.decode(nv[0]), Encode.decode(val));
}
else
{
formData.add(Encode.decode(param), "");
}
}
return formData;
}
@Override
public void saveRequest() {
@ -62,7 +362,7 @@ public class FilterSessionStore implements AdapterSessionStore {
String name = names.nextElement();
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
headers.add(name, values.nextElement());
headers.add(name.toLowerCase(), values.nextElement());
}
}
session.setAttribute(SAVED_HEADERS, headers);
@ -93,6 +393,8 @@ public class FilterSessionStore implements AdapterSessionStore {
if (body.length > 0) {
session.setAttribute(SAVED_BODY, body);
}
}
}

View file

@ -67,6 +67,13 @@ public class ServletHttpFacade implements HttpFacade {
return queryParameters.getFirst(param);
}
public MultivaluedHashMap<String, String> getQueryParameters() {
if (queryParameters == null) {
queryParameters = UriUtils.decodeQueryString(request.getQueryString());
}
return queryParameters;
}
@Override
public Cookie getCookie(String cookieName) {
if (request.getCookies() == null) return null;

View file

@ -0,0 +1,83 @@
<?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-filter-adapter</artifactId>
<name>Keycloak Servlet Filter Adapter Integration</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</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.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-xc</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,86 @@
package org.keycloak.adapters.servlet;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.OidcKeycloakAccount;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.Principal;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author <a href="mailto:ungarida@gmail.com">Davide Ungari</a>
* @version $Revision: 1 $
*/
public class FilterRequestAuthenticator extends RequestAuthenticator {
private static final Logger log = Logger.getLogger(""+FilterRequestAuthenticator.class);
protected HttpServletRequest request;
public FilterRequestAuthenticator(KeycloakDeployment deployment,
AdapterTokenStore tokenStore,
OIDCHttpFacade facade,
HttpServletRequest request,
int sslRedirectPort) {
super(facade, deployment, tokenStore, sslRedirectPort);
this.request = request;
}
@Override
protected OAuthRequestAuthenticator createOAuthAuthenticator() {
return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore);
}
@Override
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
OidcKeycloakAccount account = new OidcKeycloakAccount() {
@Override
public Principal getPrincipal() {
return skp;
}
@Override
public Set<String> getRoles() {
return roles;
}
@Override
public KeycloakSecurityContext getKeycloakSecurityContext() {
return securityContext;
}
};
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
this.tokenStore.saveAccountInfo(account);
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
if (log.isLoggable(Level.FINE)) {
log.fine("Completing bearer authentication. Bearer roles: " + roles);
}
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
}
@Override
protected String getHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}
}

View file

@ -0,0 +1,158 @@
package org.keycloak.adapters.servlet;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.AuthenticatedActionsHandler;
import org.keycloak.adapters.InMemorySessionIdMapper;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.SessionIdMapper;
import org.keycloak.adapters.UserSessionManagement;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
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.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
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 KeycloakOIDCFilter implements Filter {
protected AdapterDeploymentContext deploymentContext;
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) {
try {
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
deploymentContext = new AdapterDeploymentContext(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 fp = filterConfig.getInitParameter("keycloak.config.file");
InputStream is = null;
if (fp != null) {
try {
is = new FileInputStream(fp);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
} else {
String path = "/WEB-INF/keycloak.json";
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
if (pathParam != null) path = pathParam;
is = filterConfig.getServletContext().getResourceAsStream(path);
}
KeycloakDeployment kd;
if (is == null) {
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
kd = new KeycloakDeployment();
} else {
kd = KeycloakDeploymentBuilder.build(is);
}
deploymentContext = new AdapterDeploymentContext(kd);
log.fine("Keycloak is using a per-deployment configuration.");
}
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment == null || !deployment.isConfigured()) {
response.sendError(403);
log.fine("deployment not configured");
return;
}
PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() {
@Override
public void logoutAll() {
if (idMapper != null) {
idMapper.clear();
}
}
@Override
public void logoutHttpSessions(List<String> ids) {
for (String id : ids) {
idMapper.removeSession(id);
}
}
}, deploymentContext, facade);
if (preActions.handleRequest()) {
return;
}
OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, deployment, idMapper);
tokenStore.checkCurrentToken();
FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(deployment, tokenStore, facade, request, 8443);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
log.fine("AUTHENTICATED");
if (facade.isEnded()) {
return;
}
AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade);
if (actions.handledRequest()) {
return;
} else {
HttpServletRequestWrapper wrapper = tokenStore.buildWrapper();
chain.doFilter(wrapper, res);
return;
}
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
log.fine("challenge");
challenge.challenge(facade);
if (challenge.errorPage()) {
response.sendError(challenge.getResponseCode());
return;
}
log.fine("sending challenge");
return;
}
response.sendError(403);
}
@Override
public void destroy() {
}
}

View file

@ -0,0 +1,167 @@
package org.keycloak.adapters.servlet;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OidcKeycloakAccount;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.adapters.SessionIdMapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.security.Principal;
import java.util.Set;
import java.util.logging.Logger;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OIDCFilterSessionStore extends FilterSessionStore implements AdapterTokenStore {
protected final KeycloakDeployment deployment;
private static final Logger log = Logger.getLogger("" + OIDCFilterSessionStore.class);
protected final SessionIdMapper idMapper;
public OIDCFilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, KeycloakDeployment deployment, SessionIdMapper idMapper) {
super(request, facade, maxBuffer);
this.deployment = deployment;
this.idMapper = idMapper;
}
public HttpServletRequestWrapper buildWrapper() {
HttpSession session = request.getSession();
KeycloakAccount account = (KeycloakAccount)session.getAttribute((KeycloakAccount.class.getName()));
return buildWrapper(session, account);
}
@Override
public void checkCurrentToken() {
HttpSession httpSession = request.getSession(false);
if (httpSession == null) return;
SerializableKeycloakAccount account = (SerializableKeycloakAccount)httpSession.getAttribute(KeycloakAccount.class.getName());
if (account == null) {
return;
}
RefreshableKeycloakSecurityContext session = account.getKeycloakSecurityContext();
if (session == null) return;
// just in case session got serialized
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
// not be updated
boolean success = session.refreshExpiredToken(false);
if (success && session.isActive()) return;
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
//log.fine("Cleanup and expire session " + httpSession.getId() + " after failed refresh");
cleanSession(httpSession);
httpSession.invalidate();
}
protected void cleanSession(HttpSession session) {
session.removeAttribute(KeycloakAccount.class.getName());
clearSavedRequest(session);
}
@Override
public boolean isCached(RequestAuthenticator authenticator) {
HttpSession httpSession = request.getSession(false);
if (httpSession == null) return false;
SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName());
if (account == null) {
return false;
}
log.fine("remote logged in already. Establish state from session");
RefreshableKeycloakSecurityContext securityContext = account.getKeycloakSecurityContext();
if (!deployment.getRealm().equals(securityContext.getRealm())) {
log.fine("Account from cookie is from a different realm than for the request.");
cleanSession(httpSession);
return false;
}
if (idMapper != null && !idMapper.hasSession(httpSession.getId())) {
cleanSession(httpSession);
return false;
}
securityContext.setCurrentRequestInfo(deployment, this);
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
needRequestRestore = restoreRequest();
return true;
}
public static class SerializableKeycloakAccount implements OidcKeycloakAccount, Serializable {
protected Set<String> roles;
protected Principal principal;
protected RefreshableKeycloakSecurityContext securityContext;
public SerializableKeycloakAccount(Set<String> roles, Principal principal, RefreshableKeycloakSecurityContext securityContext) {
this.roles = roles;
this.principal = principal;
this.securityContext = securityContext;
}
@Override
public Principal getPrincipal() {
return principal;
}
@Override
public Set<String> getRoles() {
return roles;
}
@Override
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
return securityContext;
}
}
@Override
public void saveAccountInfo(OidcKeycloakAccount account) {
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext();
Set<String> roles = account.getRoles();
SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext);
HttpSession httpSession = request.getSession();
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId());
//String username = securityContext.getToken().getSubject();
//log.fine("userSessionManagement.login: " + username);
}
@Override
public void logout() {
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName());
if (account != null) {
account.getKeycloakSecurityContext().logout(deployment);
}
cleanSession(httpSession);
}
}
@Override
public void servletRequestLogout() {
logout();
}
@Override
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
// no-op
}
}

View file

@ -0,0 +1,23 @@
package org.keycloak.adapters.servlet;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OIDCServletHttpFacade extends ServletHttpFacade implements OIDCHttpFacade {
public OIDCServletHttpFacade(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
}
@Override
public KeycloakSecurityContext getSecurityContext() {
return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
}
}

View file

@ -77,6 +77,13 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
request.setUserPrincipal(null);
}
protected void beforeStop() {
if (nodesRegistrationManagement != null) {
nodesRegistrationManagement.stop();
}
}
@SuppressWarnings("UseSpecificCatch")
public void keycloakInit() {
// Possible scenarios:
@ -119,11 +126,6 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
nodesRegistrationManagement = new NodesRegistrationManagement();
}
protected void beforeStop() {
if (nodesRegistrationManagement != null) {
nodesRegistrationManagement.stop();
}
}
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);

View file

@ -900,7 +900,12 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-server-filter-adapter</artifactId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View file

@ -31,6 +31,11 @@ public class InitiateLogin implements AuthChallenge {
return false;
}
@Override
public int getResponseCode() {
return 0;
}
@Override
public boolean challenge(HttpFacade httpFacade) {
try {

View file

@ -259,6 +259,7 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
if (challenge.errorPage() && errorPage != null) {
Response response = (Response)res;
try {
@ -268,7 +269,6 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
}
}
challenge.challenge(facade);
}
return Authentication.SEND_CONTINUE;
}

View file

@ -9,7 +9,7 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-saml-server-filter-adapter</artifactId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
<name>Keycloak SAML Servlet Filter</name>
<description />

View file

@ -2,6 +2,7 @@ package org.keycloak.adapters.saml.servlet;
import org.jboss.logging.Logger;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.SessionIdMapper;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
@ -34,8 +35,6 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
this.idMapper = idMapper;
}
protected boolean needRequestRestore;
@Override
public void logoutAccount() {
HttpSession session = request.getSession(false);
@ -107,91 +106,8 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
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();
}
};
}
final KeycloakAccount account = samlSession;
return buildWrapper(session, account);
}
@Override

View file

@ -24,6 +24,8 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
@ -51,11 +53,21 @@ public class SamlFilter implements Filter {
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 fp = filterConfig.getInitParameter("keycloak.config.file");
InputStream is = null;
if (fp != null) {
try {
is = new FileInputStream(fp);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
} else {
String path = "/WEB-INF/keycloak-saml.xml";
String pathParam = filterConfig.getInitParameter("keycloak.config.file");
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
if (pathParam != null) path = pathParam;
InputStream is = filterConfig.getServletContext().getResourceAsStream(path);
is = filterConfig.getServletContext().getResourceAsStream(path);
}
final SamlDeployment deployment;
if (is == null) {
log.info("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
@ -124,19 +136,17 @@ public class SamlFilter implements Filter {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
log.fine("challenge");
challenge.challenge(facade);
if (challenge.errorPage()) {
response.sendError(403);
response.sendError(challenge.getResponseCode());
return;
}
log.fine("sending challenge");
challenge.challenge(facade);
}
if (outcome == AuthOutcome.FAILED) {
response.sendError(403);
} else if (!facade.isEnded()) {
chain.doFilter(req, res);
return;
}
if (!facade.isEnded()) {
response.sendError(403);
}
}

View file

@ -212,14 +212,12 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
if (loginConfig == null) {
loginConfig = request.getContext().getLoginConfig();
}
challenge.challenge(facade);
if (challenge.errorPage()) {
log.fine("error page");
if (forwardToErrorPageInternal(request, response, loginConfig))return false;
}
log.fine("sending challenge");
challenge.challenge(facade);
}
log.fine("No challenge, but failed authentication");
return false;
}

View file

@ -107,7 +107,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-server-filter-adapter</artifactId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -604,6 +604,7 @@ public class AdapterTestStrategy extends ExternalResource {
// logout sessions in account management
accountSessionsPage.realm("demo");
accountSessionsPage.open();
Assert.assertTrue(accountSessionsPage.isCurrent());
accountSessionsPage.logoutAll();
// Assert I need to login again (logout was propagated to the app)

View file

@ -0,0 +1,214 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite.adapter;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import java.net.URL;
import java.security.PublicKey;
/**
* Tests Undertow Adapter
*
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
public class FilterAdapterTest {
public static PublicKey realmPublicKey;
@ClassRule
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
RealmModel realm = AdapterTestStrategy.baseAdapterTestInitialization(session, manager, adminRealm, getClass());
realmPublicKey = realm.getPublicKey();
URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
createApplicationDeployment()
.name("customer-portal").contextPath("/customer-portal")
.servletClass(CustomerServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplicationWithFilter();
url = getClass().getResource("/adapter-test/secure-portal-keycloak.json");
createApplicationDeployment()
.name("secure-portal").contextPath("/secure-portal")
.servletClass(CallAuthenticatedServlet.class).adapterConfigPath(url.getPath())
.role("user")
.isConstrained(false).deployApplicationWithFilter();
url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
createApplicationDeployment()
.name("customer-db").contextPath("/customer-db")
.servletClass(CustomerDatabaseServlet.class).adapterConfigPath(url.getPath())
.role("user")
.errorPage(null).deployApplicationWithFilter();
createApplicationDeployment()
.name("customer-db-error-page").contextPath("/customer-db-error-page")
.servletClass(CustomerDatabaseServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplicationWithFilter();
url = getClass().getResource("/adapter-test/product-keycloak.json");
createApplicationDeployment()
.name("product-portal").contextPath("/product-portal")
.servletClass(ProductServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplicationWithFilter();
// Test that replacing system properties works for adapters
System.setProperty("app.server.base.url", "http://localhost:8081");
System.setProperty("my.host.name", "localhost");
url = getClass().getResource("/adapter-test/session-keycloak.json");
createApplicationDeployment()
.name("session-portal").contextPath("/session-portal")
.servletClass(SessionServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplicationWithFilter();
url = getClass().getResource("/adapter-test/input-keycloak.json");
createApplicationDeployment()
.name("input-portal").contextPath("/input-portal")
.servletClass(InputServlet.class).adapterConfigPath(url.getPath())
.role("user").constraintUrl("/secured/*").deployApplicationWithFilter();
}
};
@Rule
public AdapterTestStrategy testStrategy = new AdapterTestStrategy("http://localhost:8081/auth", "http://localhost:8081", keycloakRule);
@Test
public void testLoginSSOAndLogout() throws Exception {
testStrategy.testLoginSSOAndLogout();
}
@Test
public void testSavedPostRequest() throws Exception {
testStrategy.testSavedPostRequest();
}
@Test
public void testServletRequestLogout() throws Exception {
testStrategy.testServletRequestLogout();
}
@Test
public void testLoginSSOIdle() throws Exception {
testStrategy.testLoginSSOIdle();
}
@Test
public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception {
testStrategy.testLoginSSOIdleRemoveExpiredUserSessions();
}
@Test
public void testLoginSSOMax() throws Exception {
testStrategy.testLoginSSOMax();
}
/**
* KEYCLOAK-518
* @throws Exception
*/
@Test
public void testNullBearerToken() throws Exception {
testStrategy.testNullBearerToken();
}
/**
* KEYCLOAK-1368
* @throws Exception
*/
/*
can't test because of the way filter works
@Test
public void testNullBearerTokenCustomErrorPage() throws Exception {
testStrategy.testNullBearerTokenCustomErrorPage();
}
*/
/**
* KEYCLOAK-518
* @throws Exception
*/
@Test
public void testBadUser() throws Exception {
testStrategy.testBadUser();
}
@Test
public void testVersion() throws Exception {
testStrategy.testVersion();
}
/*
Don't need to test this because HttpServletRequest.authenticate doesn't make sense with filter implementation
@Test
public void testAuthenticated() throws Exception {
testStrategy.testAuthenticated();
}
*/
/**
* KEYCLOAK-732
*
* @throws Throwable
*/
@Test
public void testSingleSessionInvalidated() throws Throwable {
testStrategy.testSingleSessionInvalidated();
}
/**
* KEYCLOAK-741
*/
@Test
public void testSessionInvalidatedAfterFailedRefresh() throws Throwable {
testStrategy.testSessionInvalidatedAfterFailedRefresh();
}
/**
* KEYCLOAK-942
*/
@Test
public void testAdminApplicationLogout() throws Throwable {
testStrategy.testAdminApplicationLogout();
}
/**
* KEYCLOAK-1216
*/
/*
Can't test this because backchannel logout for filter does not invalidate the session
@Test
public void testAccountManagementSessionsLogout() throws Throwable {
testStrategy.testAccountManagementSessionsLogout();
}
*/
}

View file

@ -1,6 +1,7 @@
package org.keycloak.testsuite.rule;
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.SecurityInfo;
@ -11,6 +12,8 @@ import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;
import org.keycloak.Config;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.saml.servlet.SamlFilter;
import org.keycloak.adapters.servlet.KeycloakOIDCFilter;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
@ -24,6 +27,7 @@ import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.Time;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.ws.rs.core.Application;
import java.io.ByteArrayOutputStream;
@ -350,6 +354,22 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
server.getServer().deploy(di);
}
public void deployApplicationWithFilter() {
DeploymentInfo di = createDeploymentInfo(name, contextPath, servletClass);
FilterInfo filter = new FilterInfo("keycloak-filter", KeycloakOIDCFilter.class);
if (null == keycloakConfigResolver) {
filter.addInitParam("keycloak.config.file", adapterConfigPath);
} else {
filter.addInitParam("keycloak.config.resolver", keycloakConfigResolver.getCanonicalName());
}
di.addFilter(filter);
di.addFilterUrlMapping("keycloak-filter", constraintUrl, DispatcherType.REQUEST);
server.getServer().deploy(di);
}
}
}