/* * 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.tomcat; import org.apache.catalina.Context; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Manager; 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.KeycloakSecurityContext; import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterTokenStore; 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.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.constants.AdapterConstants; import org.keycloak.enums.TokenStore; 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; /** * 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 AbstractKeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener { public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; private final static Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected AdapterDeploymentContext deploymentContext; protected NodesRegistrationManagement nodesRegistrationManagement; @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 (event.getType() == Lifecycle.BEFORE_STOP_EVENT) { beforeStop(); } } protected void logoutInternal(Request request) { KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); if (ksc != null) { CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, null); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); if (ksc instanceof RefreshableKeycloakSecurityContext) { ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); } AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); tokenStore.logout(); request.removeAttribute(KeycloakSecurityContext.class.getName()); } request.setUserPrincipal(null); } protected void beforeStop() { if (nodesRegistrationManagement != null) { nodesRegistrationManagement.stop(); } } @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 { KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); deploymentContext = new AdapterDeploymentContext(configResolver); log.debugv("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 configInputStream = getConfigInputStream(context); KeycloakDeployment kd; if (configInputStream == null) { log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); kd = new KeycloakDeployment(); } else { kd = KeycloakDeploymentBuilder.build(configInputStream); } deploymentContext = new AdapterDeploymentContext(kd); log.debug("Keycloak is using a per-deployment configuration."); } context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer()); setNext(actions); nodesRegistrationManagement = new NodesRegistrationManagement(); } private static InputStream getJSONFromServletContext(ServletContext servletContext) { String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); if (json == null) { return null; } log.trace("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME); return new ByteArrayInputStream(json.getBytes()); } private static InputStream getConfigInputStream(Context context) { InputStream is = getJSONFromServletContext(context.getServletContext()); if (is == null) { String path = context.getServletContext().getInitParameter("keycloak.config.file"); if (path == null) { log.trace("**** using /WEB-INF/keycloak.json"); is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json"); } 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 { try { CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); Manager sessionManager = request.getContext().getManager(); CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(userSessionManagement, sessionManager); PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, deploymentContext, facade); if (handler.handleRequest()) { return; } checkKeycloakSession(request, facade); super.invoke(request, response); } finally { } } protected abstract GenericPrincipalFactory createPrincipalFactory(); protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException; protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); if (deployment == null || !deployment.isConfigured()) { //needed for the EAP6/AS7 adapter relying on the tomcat core adapter facade.getResponse().sendError(401); return false; } AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); nodesRegistrationManagement.tryRegister(deployment); CatalinaRequestAuthenticator authenticator = createRequestAuthenticator(request, facade, deployment, tokenStore); AuthOutcome outcome = authenticator.authenticate(); if (outcome == AuthOutcome.AUTHENTICATED) { if (facade.isEnded()) { return false; } return true; } AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { challenge.challenge(facade); } return false; } protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) { return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, createPrincipalFactory()); } /** * Checks that access token is still valid. Will attempt refresh of token if it is not. * * @param request */ protected void checkKeycloakSession(Request request, HttpFacade facade) { KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); tokenStore.checkCurrentToken(); } 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 AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE); if (store != null) { return store; } if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) { store = createSessionTokenStore(request, resolvedDeployment); } else { store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, createPrincipalFactory()); } request.setNote(TOKEN_STORE_NOTE, store); return store; } private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) { AdapterTokenStore store; store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement, createPrincipalFactory(), this); return store; } }