Merge pull request #1702 from patriot1burke/master
SAML and OIDC Servlet Filter SP
This commit is contained in:
commit
4acc69c604
26 changed files with 1163 additions and 114 deletions
|
@ -114,6 +114,11 @@ public class BearerTokenRequestAuthenticator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade exchange) {
|
public boolean challenge(HttpFacade exchange) {
|
||||||
// do the same thing as client cert auth
|
// do the same thing as client cert auth
|
||||||
|
@ -139,6 +144,11 @@ public class BearerTokenRequestAuthenticator {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade facade) {
|
public boolean challenge(HttpFacade facade) {
|
||||||
facade.getResponse().setStatus(401);
|
facade.getResponse().setStatus(401);
|
||||||
|
|
|
@ -181,6 +181,11 @@ public class OAuthRequestAuthenticator {
|
||||||
public boolean errorPage() {
|
public boolean errorPage() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return new AuthChallenge() {
|
return new AuthChallenge() {
|
||||||
|
@ -190,6 +195,11 @@ public class OAuthRequestAuthenticator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade exchange) {
|
public boolean challenge(HttpFacade exchange) {
|
||||||
tokenStore.saveRequest();
|
tokenStore.saveRequest();
|
||||||
|
@ -262,6 +272,11 @@ public class OAuthRequestAuthenticator {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade exchange) {
|
public boolean challenge(HttpFacade exchange) {
|
||||||
exchange.getResponse().setStatus(code);
|
exchange.getResponse().setStatus(code);
|
||||||
|
|
|
@ -13,9 +13,17 @@ public interface AuthChallenge {
|
||||||
boolean challenge(HttpFacade exchange);
|
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
|
* @return
|
||||||
*/
|
*/
|
||||||
boolean errorPage();
|
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);
|
return sessionToSso.containsKey(id) || sessionToPrincipal.containsKey(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
ssoToSession.clear();
|
||||||
|
sessionToSso.clear();
|
||||||
|
principalToSession.clear();
|
||||||
|
sessionToPrincipal.clear();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getUserSessions(String principal) {
|
public Set<String> getUserSessions(String principal) {
|
||||||
Set<String> lookup = principalToSession.get(principal);
|
Set<String> lookup = principalToSession.get(principal);
|
||||||
|
|
|
@ -9,6 +9,8 @@ import java.util.Set;
|
||||||
public interface SessionIdMapper {
|
public interface SessionIdMapper {
|
||||||
boolean hasSession(String id);
|
boolean hasSession(String id);
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
Set<String> getUserSessions(String principal);
|
Set<String> getUserSessions(String principal);
|
||||||
|
|
||||||
String getSessionFromSSO(String sso);
|
String getSessionFromSSO(String sso);
|
||||||
|
|
|
@ -262,16 +262,16 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
|
||||||
}
|
}
|
||||||
AuthChallenge challenge = authenticator.getChallenge();
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
if (challenge != null) {
|
if (challenge != null) {
|
||||||
|
challenge.challenge(facade);
|
||||||
if (challenge.errorPage() && errorPage != null) {
|
if (challenge.errorPage() && errorPage != null) {
|
||||||
Response response = (Response)res;
|
Response response = (Response)res;
|
||||||
try {
|
try {
|
||||||
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
|
response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
challenge.challenge(facade);
|
|
||||||
}
|
}
|
||||||
return Authentication.SEND_CONTINUE;
|
return Authentication.SEND_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
<module>undertow-adapter-spi</module>
|
<module>undertow-adapter-spi</module>
|
||||||
<module>undertow</module>
|
<module>undertow</module>
|
||||||
<module>wildfly</module>
|
<module>wildfly</module>
|
||||||
|
<module>servlet-filter</module>
|
||||||
<module>js</module>
|
<module>js</module>
|
||||||
<module>installed</module>
|
<module>installed</module>
|
||||||
<module>admin-client</module>
|
<module>admin-client</module>
|
||||||
|
|
|
@ -2,14 +2,32 @@ package org.keycloak.adapters.servlet;
|
||||||
|
|
||||||
import org.keycloak.adapters.AdapterSessionStore;
|
import org.keycloak.adapters.AdapterSessionStore;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.util.Encode;
|
||||||
import org.keycloak.util.MultivaluedHashMap;
|
import org.keycloak.util.MultivaluedHashMap;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletInputStream;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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.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>
|
* @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 HttpFacade facade;
|
||||||
protected final int maxBuffer;
|
protected final int maxBuffer;
|
||||||
protected byte[] restoredBuffer = null;
|
protected byte[] restoredBuffer = null;
|
||||||
|
protected boolean needRequestRestore;
|
||||||
|
|
||||||
public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) {
|
public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) {
|
||||||
this.request = request;
|
this.request = request;
|
||||||
|
@ -38,6 +57,249 @@ public class FilterSessionStore implements AdapterSessionStore {
|
||||||
session.removeAttribute(SAVED_BODY);
|
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() {
|
public String getRedirectUri() {
|
||||||
HttpSession session = request.getSession(true);
|
HttpSession session = request.getSession(true);
|
||||||
return (String)session.getAttribute(REDIRECT_URI);
|
return (String)session.getAttribute(REDIRECT_URI);
|
||||||
|
@ -50,6 +312,44 @@ public class FilterSessionStore implements AdapterSessionStore {
|
||||||
return session.getAttribute(REDIRECT_URI) != null;
|
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
|
@Override
|
||||||
public void saveRequest() {
|
public void saveRequest() {
|
||||||
|
@ -62,7 +362,7 @@ public class FilterSessionStore implements AdapterSessionStore {
|
||||||
String name = names.nextElement();
|
String name = names.nextElement();
|
||||||
Enumeration<String> values = request.getHeaders(name);
|
Enumeration<String> values = request.getHeaders(name);
|
||||||
while (values.hasMoreElements()) {
|
while (values.hasMoreElements()) {
|
||||||
headers.add(name, values.nextElement());
|
headers.add(name.toLowerCase(), values.nextElement());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
session.setAttribute(SAVED_HEADERS, headers);
|
session.setAttribute(SAVED_HEADERS, headers);
|
||||||
|
@ -93,6 +393,8 @@ public class FilterSessionStore implements AdapterSessionStore {
|
||||||
if (body.length > 0) {
|
if (body.length > 0) {
|
||||||
session.setAttribute(SAVED_BODY, body);
|
session.setAttribute(SAVED_BODY, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,13 @@ public class ServletHttpFacade implements HttpFacade {
|
||||||
return queryParameters.getFirst(param);
|
return queryParameters.getFirst(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MultivaluedHashMap<String, String> getQueryParameters() {
|
||||||
|
if (queryParameters == null) {
|
||||||
|
queryParameters = UriUtils.decodeQueryString(request.getQueryString());
|
||||||
|
}
|
||||||
|
return queryParameters;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cookie getCookie(String cookieName) {
|
public Cookie getCookie(String cookieName) {
|
||||||
if (request.getCookies() == null) return null;
|
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,162 @@
|
||||||
|
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.NodesRegistrationManagement;
|
||||||
|
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();
|
||||||
|
protected NodesRegistrationManagement nodesRegistrationManagement;
|
||||||
|
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);
|
||||||
|
nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nodesRegistrationManagement.tryRegister(deployment);
|
||||||
|
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);
|
request.setUserPrincipal(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void beforeStop() {
|
||||||
|
if (nodesRegistrationManagement != null) {
|
||||||
|
nodesRegistrationManagement.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("UseSpecificCatch")
|
@SuppressWarnings("UseSpecificCatch")
|
||||||
public void keycloakInit() {
|
public void keycloakInit() {
|
||||||
// Possible scenarios:
|
// Possible scenarios:
|
||||||
|
@ -119,11 +126,6 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
||||||
nodesRegistrationManagement = new NodesRegistrationManagement();
|
nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void beforeStop() {
|
|
||||||
if (nodesRegistrationManagement != null) {
|
|
||||||
nodesRegistrationManagement.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
|
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
|
||||||
String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);
|
String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);
|
||||||
|
|
7
pom.xml
7
pom.xml
|
@ -894,7 +894,12 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<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>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -31,6 +31,11 @@ public class InitiateLogin implements AuthChallenge {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade httpFacade) {
|
public boolean challenge(HttpFacade httpFacade) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -259,6 +259,7 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
|
||||||
|
|
||||||
AuthChallenge challenge = authenticator.getChallenge();
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
if (challenge != null) {
|
if (challenge != null) {
|
||||||
|
challenge.challenge(facade);
|
||||||
if (challenge.errorPage() && errorPage != null) {
|
if (challenge.errorPage() && errorPage != null) {
|
||||||
Response response = (Response)res;
|
Response response = (Response)res;
|
||||||
try {
|
try {
|
||||||
|
@ -268,7 +269,6 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
challenge.challenge(facade);
|
|
||||||
}
|
}
|
||||||
return Authentication.SEND_CONTINUE;
|
return Authentication.SEND_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<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>
|
<name>Keycloak SAML Servlet Filter</name>
|
||||||
<description />
|
<description />
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.keycloak.adapters.saml.servlet;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
import org.keycloak.adapters.SessionIdMapper;
|
import org.keycloak.adapters.SessionIdMapper;
|
||||||
import org.keycloak.adapters.saml.SamlSession;
|
import org.keycloak.adapters.saml.SamlSession;
|
||||||
import org.keycloak.adapters.saml.SamlSessionStore;
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
@ -34,8 +35,6 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
|
||||||
this.idMapper = idMapper;
|
this.idMapper = idMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean needRequestRestore;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logoutAccount() {
|
public void logoutAccount() {
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
|
@ -107,91 +106,8 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
|
||||||
public HttpServletRequestWrapper getWrap() {
|
public HttpServletRequestWrapper getWrap() {
|
||||||
HttpSession session = request.getSession(true);
|
HttpSession session = request.getSession(true);
|
||||||
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
|
||||||
if (needRequestRestore) {
|
final KeycloakAccount account = samlSession;
|
||||||
final String method = (String)session.getAttribute(SAVED_METHOD);
|
return buildWrapper(session, account);
|
||||||
final byte[] body = (byte[])session.getAttribute(SAVED_BODY);
|
|
||||||
final MultivaluedHashMap<String, String> headers = (MultivaluedHashMap<String, String>)session.getAttribute(SAVED_HEADERS);
|
|
||||||
clearSavedRequest(session);
|
|
||||||
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
|
|
||||||
@Override
|
|
||||||
public boolean isUserInRole(String role) {
|
|
||||||
return samlSession.getRoles().contains(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Principal getUserPrincipal() {
|
|
||||||
return samlSession.getPrincipal();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMethod() {
|
|
||||||
if (needRequestRestore) {
|
|
||||||
return method;
|
|
||||||
} else {
|
|
||||||
return super.getMethod();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getHeader(String name) {
|
|
||||||
if (needRequestRestore && headers != null) {
|
|
||||||
return headers.getFirst(name);
|
|
||||||
}
|
|
||||||
return super.getHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Enumeration<String> getHeaders(String name) {
|
|
||||||
if (needRequestRestore && headers != null) {
|
|
||||||
List<String> values = headers.getList(name);
|
|
||||||
if (values == null) return Collections.emptyEnumeration();
|
|
||||||
else return Collections.enumeration(values);
|
|
||||||
}
|
|
||||||
return super.getHeaders(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Enumeration<String> getHeaderNames() {
|
|
||||||
if (needRequestRestore && headers != null) {
|
|
||||||
return Collections.enumeration(headers.keySet());
|
|
||||||
}
|
|
||||||
return super.getHeaderNames();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ServletInputStream getInputStream() throws IOException {
|
|
||||||
|
|
||||||
if (needRequestRestore && body != null) {
|
|
||||||
final ByteArrayInputStream is = new ByteArrayInputStream(body);
|
|
||||||
return new ServletInputStream() {
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
return is.read();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return super.getInputStream();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return wrapper;
|
|
||||||
} else {
|
|
||||||
return new HttpServletRequestWrapper(request) {
|
|
||||||
@Override
|
|
||||||
public boolean isUserInRole(String role) {
|
|
||||||
return samlSession.getRoles().contains(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Principal getUserPrincipal() {
|
|
||||||
return samlSession.getPrincipal();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -24,6 +24,8 @@ import javax.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequestWrapper;
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -52,10 +54,20 @@ public class SamlFilter implements Filter {
|
||||||
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String path = "/WEB-INF/keycloak-saml.xml";
|
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
||||||
String pathParam = filterConfig.getInitParameter("keycloak.config.file");
|
InputStream is = null;
|
||||||
if (pathParam != null) path = pathParam;
|
if (fp != null) {
|
||||||
InputStream is = filterConfig.getServletContext().getResourceAsStream(path);
|
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;
|
final SamlDeployment deployment;
|
||||||
if (is == null) {
|
if (is == null) {
|
||||||
log.info("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
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();
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
if (challenge != null) {
|
if (challenge != null) {
|
||||||
log.fine("challenge");
|
log.fine("challenge");
|
||||||
|
challenge.challenge(facade);
|
||||||
if (challenge.errorPage()) {
|
if (challenge.errorPage()) {
|
||||||
response.sendError(403);
|
response.sendError(challenge.getResponseCode());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.fine("sending challenge");
|
log.fine("sending challenge");
|
||||||
challenge.challenge(facade);
|
|
||||||
}
|
|
||||||
if (outcome == AuthOutcome.FAILED) {
|
|
||||||
response.sendError(403);
|
|
||||||
} else if (!facade.isEnded()) {
|
|
||||||
chain.doFilter(req, res);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!facade.isEnded()) {
|
||||||
|
response.sendError(403);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -212,14 +212,12 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
|
||||||
if (loginConfig == null) {
|
if (loginConfig == null) {
|
||||||
loginConfig = request.getContext().getLoginConfig();
|
loginConfig = request.getContext().getLoginConfig();
|
||||||
}
|
}
|
||||||
|
challenge.challenge(facade);
|
||||||
if (challenge.errorPage()) {
|
if (challenge.errorPage()) {
|
||||||
log.fine("error page");
|
log.fine("error page");
|
||||||
if (forwardToErrorPageInternal(request, response, loginConfig))return false;
|
if (forwardToErrorPageInternal(request, response, loginConfig))return false;
|
||||||
}
|
}
|
||||||
log.fine("sending challenge");
|
|
||||||
challenge.challenge(facade);
|
|
||||||
}
|
}
|
||||||
log.fine("No challenge, but failed authentication");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,11 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
|
@ -604,6 +604,7 @@ public class AdapterTestStrategy extends ExternalResource {
|
||||||
// logout sessions in account management
|
// logout sessions in account management
|
||||||
accountSessionsPage.realm("demo");
|
accountSessionsPage.realm("demo");
|
||||||
accountSessionsPage.open();
|
accountSessionsPage.open();
|
||||||
|
Assert.assertTrue(accountSessionsPage.isCurrent());
|
||||||
accountSessionsPage.logoutAll();
|
accountSessionsPage.logoutAll();
|
||||||
|
|
||||||
// Assert I need to login again (logout was propagated to the app)
|
// 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;
|
package org.keycloak.testsuite.rule;
|
||||||
|
|
||||||
import io.undertow.servlet.api.DeploymentInfo;
|
import io.undertow.servlet.api.DeploymentInfo;
|
||||||
|
import io.undertow.servlet.api.FilterInfo;
|
||||||
import io.undertow.servlet.api.LoginConfig;
|
import io.undertow.servlet.api.LoginConfig;
|
||||||
import io.undertow.servlet.api.SecurityConstraint;
|
import io.undertow.servlet.api.SecurityConstraint;
|
||||||
import io.undertow.servlet.api.SecurityInfo;
|
import io.undertow.servlet.api.SecurityInfo;
|
||||||
|
@ -11,6 +12,8 @@ import org.junit.rules.ExternalResource;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
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.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakTransaction;
|
import org.keycloak.models.KeycloakTransaction;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -24,6 +27,7 @@ import org.keycloak.testsuite.KeycloakServer;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.util.Time;
|
import org.keycloak.util.Time;
|
||||||
|
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
import javax.servlet.Servlet;
|
import javax.servlet.Servlet;
|
||||||
import javax.ws.rs.core.Application;
|
import javax.ws.rs.core.Application;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -350,6 +354,22 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
|
||||||
server.getServer().deploy(di);
|
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