saml and oidc filters
This commit is contained in:
parent
d39aee0a72
commit
9ced56d8d7
26 changed files with 1159 additions and 114 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -262,16 +262,16 @@ 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 {
|
||||
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
|
||||
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
challenge.challenge(facade);
|
||||
}
|
||||
return Authentication.SEND_CONTINUE;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
83
integration/servlet-filter/pom.xml
Executable file
83
integration/servlet-filter/pom.xml
Executable 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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -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>
|
||||
|
|
|
@ -31,6 +31,11 @@ public class InitiateLogin implements AuthChallenge {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean challenge(HttpFacade httpFacade) {
|
||||
try {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
@ -52,10 +54,20 @@ public class SamlFilter implements Filter {
|
|||
//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);
|
||||
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.path");
|
||||
if (pathParam != null) path = pathParam;
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue