/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.saml;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.spi.*;
import org.keycloak.adapters.tomcat.CatalinaHttpFacade;
import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
import org.keycloak.saml.common.exceptions.ParsingException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
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.lang.reflect.*;
import java.util.Map;
/**
* Keycloak authentication valve
*
* @author <a href="mailto:ungarida@gmail.com">Davide Ungari</a>
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
private final static Logger log = Logger.getLogger(AbstractSamlAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected SamlDeploymentContext deploymentContext;
protected SessionIdMapper mapper = new InMemorySessionIdMapper();
protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT;
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
cache = false;
} else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
keycloakInit();
} else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
beforeStop();
}
}
protected void logoutInternal(Request request) {
CatalinaHttpFacade facade = new CatalinaHttpFacade(null, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
tokenStore.logoutAccount();
request.setUserPrincipal(null);
}
@SuppressWarnings("UseSpecificCatch")
public void keycloakInit() {
// Possible scenarios:
// 1) The deployment has a keycloak.config.resolver specified and it exists:
// Outcome: adapter uses the resolver
// 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exists, isn't a resolver, ...) :
// Outcome: adapter is left unconfigured
// 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent)
// Outcome: adapter uses it
// 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent)
// Outcome: adapter is left unconfigured
String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) {
try {
throw new RuntimeException("Not implemented yet");
//KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
//deploymentContext = new SamlDeploymentContext(configResolver);
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception ex) {
log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage());
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
InputStream is = getConfigInputStream(context);
final SamlDeployment deployment;
if (is == null) {
log.error("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
deployment = new DefaultSamlDeployment();
} else {
try {
ResourceLoader loader = new ResourceLoader() {
@Override
public InputStream getResourceAsStream(String resource) {
return context.getServletContext().getResourceAsStream(resource);
}
};
deployment = new DeploymentBuilder().build(is, loader);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
}
deploymentContext = new SamlDeploymentContext(deployment);
log.debug("Keycloak is using a per-deployment configuration.");
}
context.getServletContext().setAttribute(SamlDeploymentContext.class.getName(), deploymentContext);
addTokenStoreUpdaters();
}
protected void beforeStop() {
}
private static InputStream getConfigFromServletContext(ServletContext servletContext) {
String xml = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);
if (xml == null) {
return null;
}
log.trace("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME);
return new ByteArrayInputStream(xml.getBytes());
}
private static InputStream getConfigInputStream(Context context) {
InputStream is = getConfigFromServletContext(context.getServletContext());
if (is == null) {
String path = context.getServletContext().getInitParameter("keycloak.config.file");
if (path == null) {
log.trace("**** using /WEB-INF/keycloak-saml.xml");
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak-saml.xml");
} else {
try {
is = new FileInputStream(path);
} catch (FileNotFoundException e) {
log.errorv("NOT FOUND {0}", path);
throw new RuntimeException(e);
}
}
}
return is;
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
log.trace("*********************** SAML ************");
CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) {
if (deployment != null && deployment.isConfigured()) {
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore);
executeAuthenticator(request, response, facade, deployment, authenticator);
return;
}
}
try {
getSessionStore(request, facade, deployment).isLoggedIn(); // sets request UserPrincipal if logged in. we do this so that the UserPrincipal is available on unsecured, unconstrainted URLs
super.invoke(request, response);
} finally {
}
}
protected abstract GenericPrincipalFactory createPrincipalFactory();
protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException;
protected void forwardToLogoutPage(Request request, HttpServletResponse response,SamlDeployment deployment) {
RequestDispatcher disp = request.getRequestDispatcher(deployment.getLogoutPage());
//make sure the login page is never cached
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
try {
disp.forward(request.getRequest(), response);
} catch (ServletException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
log.trace("authenticateInternal");
CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment == null || !deployment.isConfigured()) {
log.trace("deployment not configured");
return false;
}
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
SamlAuthenticator authenticator = new CatalinaSamlAuthenticator(facade, deployment, tokenStore);
return executeAuthenticator(request, response, facade, deployment, authenticator);
}
protected boolean executeAuthenticator(Request request, HttpServletResponse response, CatalinaHttpFacade facade, SamlDeployment deployment, SamlAuthenticator authenticator) {
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
log.trace("AUTHENTICATED");
if (facade.isEnded()) {
return false;
}
return true;
}
if (outcome == AuthOutcome.LOGGED_OUT) {
logoutInternal(request);
if (deployment.getLogoutPage() != null) {
forwardToLogoutPage(request, response, deployment);
}
log.trace("Logging OUT");
return false;
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
log.trace("challenge");
challenge.challenge(facade);
}
return false;
}
public void keycloakSaveRequest(Request request) throws IOException {
saveRequest(request, request.getSessionInternal(true));
}
public boolean keycloakRestoreRequest(Request request) {
try {
return restoreRequest(request, request.getSessionInternal());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected SamlSessionStore getSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
SamlSessionStore store = (SamlSessionStore)request.getNote(TOKEN_STORE_NOTE);
if (store != null) {
return store;
}
store = createSessionStore(request, facade, resolvedDeployment);
request.setNote(TOKEN_STORE_NOTE, store);
return store;
}
protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
SamlSessionStore store;
store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, idMapperUpdater, request, this, facade, resolvedDeployment);
return store;
}
protected void addTokenStoreUpdaters() {
SessionIdMapperUpdater updater = getIdMapperUpdater();
try {
String idMapperSessionUpdaterClasses = context.getServletContext().getInitParameter("keycloak.sessionIdMapperUpdater.classes");
if (idMapperSessionUpdaterClasses == null) {
return;
}
for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) {
if (! clazz.isEmpty()) {
updater = invokeAddTokenStoreUpdaterMethod(clazz, updater);
}
}
} finally {
setIdMapperUpdater(updater);
}
}
private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, SessionIdMapperUpdater previousIdMapperUpdater) {
try {
Class<?> clazz = context.getLoader().getClassLoader().loadClass(idMapperSessionUpdaterClass);
Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", Context.class, SessionIdMapper.class, SessionIdMapperUpdater.class);
if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers())
|| ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers())
|| ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) {
log.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
}
log.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, context, mapper, previousIdMapperUpdater);
} catch (ClassNotFoundException ex) {
log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (NoSuchMethodException ex) {
log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (SecurityException ex) {
log.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (IllegalAccessException ex) {
log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (IllegalArgumentException ex) {
log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (InvocationTargetException ex) {
log.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
}
}
public SessionIdMapperUpdater getIdMapperUpdater() {
return idMapperUpdater;
}
public void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) {
this.idMapperUpdater = idMapperUpdater;
}
}