/* * 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.picketlink.http.internal; import org.jboss.logging.Logger; import org.picketlink.Identity; import org.picketlink.authentication.AuthenticationException; import org.picketlink.config.SecurityConfiguration; import org.picketlink.config.SecurityConfigurationBuilder; import org.picketlink.config.http.AuthenticationConfiguration; import org.picketlink.config.http.AuthenticationSchemeConfiguration; import org.picketlink.config.http.AuthorizationConfiguration; import org.picketlink.config.http.BasicAuthenticationConfiguration; import org.picketlink.config.http.CORSConfiguration; import org.picketlink.config.http.CustomAuthenticationConfiguration; import org.picketlink.config.http.DigestAuthenticationConfiguration; import org.picketlink.config.http.FormAuthenticationConfiguration; import org.picketlink.config.http.HttpSecurityConfiguration; import org.picketlink.config.http.HttpSecurityConfigurationException; import org.picketlink.config.http.PathConfiguration; import org.picketlink.config.http.TokenAuthenticationConfiguration; import org.picketlink.config.http.X509AuthenticationConfiguration; import org.picketlink.credential.DefaultLoginCredentials; import org.picketlink.extension.PicketLinkExtension; import org.picketlink.http.AccessDeniedException; import org.picketlink.http.AuthenticationRequiredException; import org.picketlink.http.HttpMethod; import org.picketlink.http.MethodNotAllowedException; import org.picketlink.http.authentication.HttpAuthenticationScheme; import org.picketlink.http.authorization.PathAuthorizer; import org.picketlink.http.internal.authentication.schemes.BasicAuthenticationScheme; import org.picketlink.http.internal.authentication.schemes.DigestAuthenticationScheme; import org.picketlink.http.internal.authentication.schemes.FormAuthenticationScheme; import org.picketlink.http.internal.authentication.schemes.TokenAuthenticationScheme; import org.picketlink.http.internal.authentication.schemes.X509AuthenticationScheme; import org.picketlink.http.internal.authorization.ExpressionPathAuthorizer; import org.picketlink.http.internal.authorization.GroupPathAuthorizer; import org.picketlink.http.internal.authorization.RealmPathAuthorizer; import org.picketlink.http.internal.authorization.RolePathAuthorizer; import org.picketlink.http.internal.cors.CORS; import org.picketlink.http.internal.cors.CORSRequestType; import org.picketlink.http.internal.util.RequestUtil; import org.picketlink.idm.PartitionManager; import org.picketlink.internal.el.ELProcessor; import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import static org.picketlink.config.http.OutboundRedirectConfiguration.Condition.ERROR; import static org.picketlink.config.http.OutboundRedirectConfiguration.Condition.FORBIDDEN; import static org.picketlink.config.http.OutboundRedirectConfiguration.Condition.OK; import static org.picketlink.log.BaseLog.HTTP_LOGGER; /** * @author Pedro Igor */ public class SecurityFilter implements Filter { public static final String AUTHENTICATION_ORIGINAL_PATH = SecurityFilter.class.getName() + ".authc.original.path"; @Inject private PicketLinkExtension picketLinkExtension; @Inject private Instance<PartitionManager> partitionManager; @Inject private Instance<Identity> identityInstance; @Inject private Instance<DefaultLoginCredentials> credentialsInstance; @Inject @Any private Instance<HttpAuthenticationScheme> authenticationSchemesInstance; @Inject @Any private Instance<PathAuthorizer> pathAuthorizerInstance; @Inject private HttpServletRequestProducer servletRequestProducer; @Inject private ELProcessor elProcessor; private HttpSecurityConfiguration configuration; private Map<PathConfiguration, HttpAuthenticationScheme> authenticationSchemes = new HashMap<PathConfiguration, HttpAuthenticationScheme>(); private PathMatcher pathMatcher; private Map<PathConfiguration, List<PathAuthorizer>> pathAuthorizers = new HashMap<PathConfiguration, List<PathAuthorizer>>(); @Override public void init(FilterConfig config) throws ServletException { SecurityConfigurationBuilder configurationBuilder = this.picketLinkExtension.getSecurityConfigurationBuilder(); SecurityConfiguration securityConfiguration = configurationBuilder.build(); this.configuration = securityConfiguration.getHttpSecurityConfiguration(); if (this.configuration == null) { throw new HttpSecurityConfigurationException("No configuration provided."); } initializePathMatcher(); initializeAuthenticationSchemes(); initializePathAuthorizers(); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { if (!HttpServletRequest.class.isInstance(servletRequest)) { throw new ServletException("This filter can only process HttpServletRequest requests."); } PathConfiguration pathConfiguration = null; HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; try { Identity identity = getIdentity(); request = createRequestWrapper(request, identity); if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Processing request to path [%s].", request.getRequestURI()); } pathConfiguration = this.pathMatcher.matches(request); if (!performCORSAuthorizationIfRequired(pathConfiguration, request, response)) { return; } performAuthenticationIfRequired(pathConfiguration, identity, request, response); if (isSecured(pathConfiguration)) { if (!isMethodAllowed(pathConfiguration, request)) { throw new MethodNotAllowedException("The given method is not allowed [" + request.getMethod() + "] for path [" + pathConfiguration.getUri() + "]."); } if (!response.isCommitted()) { if (!identity.isLoggedIn()) { challengeClientForCredentials(pathConfiguration, request, response); } else if (isLogoutPath(pathConfiguration)) { performLogout(request, response, identity, pathConfiguration); } else { if (!isAuthorized(pathConfiguration, request, response)) { throw new AccessDeniedException("The request for the given path [" + pathConfiguration.getUri() + "] was forbidden."); } } } } performOutboundProcessing(pathConfiguration, request, response, chain); } catch (Exception e) { handleException(pathConfiguration, request, response, e); } } private boolean isSecured(PathConfiguration pathConfiguration) { return pathConfiguration != null && pathConfiguration.isSecured(); } private boolean isMethodAllowed(PathConfiguration pathConfiguration, HttpServletRequest request) { Set<HttpMethod> methods = pathConfiguration.getMethods(); return methods.contains(HttpMethod.valueOf(request.getMethod().toUpperCase())); } private void performOutboundProcessing(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (response.isCommitted()) { if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Response already commited. Ignoring outbound processing for path [%s].", pathConfiguration); } return; } if (isSecured(pathConfiguration)) { String redirectUrl = pathConfiguration.getRedirectUrl(OK); if (isLogoutPath(pathConfiguration)) { if (RequestUtil.isAjaxRequest(request)) { response.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { if (redirectUrl == null) { redirectUrl = request.getContextPath(); } } } if (redirectUrl != null) { redirect(redirectUrl, request, response); } else { processRequest(pathConfiguration, request, response, chain); } } else { if (this.configuration.isPermissive()) { processRequest(pathConfiguration, request, response, chain); } else if (pathConfiguration == null) { response.sendError(SC_FORBIDDEN, "No configuration found for the given path [" + request.getRequestURI() + "] "); } } } private void redirect(String redirectUrl, HttpServletRequest request, HttpServletResponse response) throws IOException { redirectUrl = formatRedirectUrl(request, redirectUrl); if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Redirecting to [%s].", redirectUrl); } response.sendRedirect(redirectUrl); } private void handleException(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response, Throwable exception) throws IOException { String redirectUrl = pathConfiguration.getRedirectUrl(exception.getClass()); Throwable cause = exception.getCause(); if (redirectUrl == null && cause != null) { redirectUrl = pathConfiguration.getRedirectUrl(cause.getClass()); } int statusCode; if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Handling exception [%s] for path [%s].", exception, request.getRequestURI()); } if (redirectUrl != null) { redirect(redirectUrl, request, response); } else { if (AuthenticationRequiredException.class.isInstance(exception)) { statusCode = SC_UNAUTHORIZED; } else if (AccessDeniedException.class.isInstance(exception)) { statusCode = SC_FORBIDDEN; if (isSecured(pathConfiguration)) { redirectUrl = pathConfiguration.getRedirectUrl(FORBIDDEN); } } else if (MethodNotAllowedException.class.isInstance(exception)) { statusCode = SC_METHOD_NOT_ALLOWED; } else { statusCode = SC_INTERNAL_SERVER_ERROR; if (isSecured(pathConfiguration)) { redirectUrl = pathConfiguration.getRedirectUrl(ERROR); } } if (redirectUrl != null) { redirect(redirectUrl, request, response); } else { String message = exception.getMessage(); if (message == null) { message = "The server could not process your request."; } if (HTTP_LOGGER.isEnabled(Logger.Level.ERROR)) { HTTP_LOGGER.errorf(exception, "Exception thrown during processing for path [%s]. Sending error with status code [%s].", request.getRequestURI(), statusCode); } response.sendError(statusCode, message); } } } private String formatRedirectUrl(HttpServletRequest request, String redirectUrl) { if (redirectUrl.startsWith("/")) { if (!redirectUrl.startsWith(request.getContextPath())) { redirectUrl = request.getContextPath() + redirectUrl; } } return redirectUrl; } private void performLogout(HttpServletRequest request, HttpServletResponse response, Identity identity, PathConfiguration pathConfiguration) throws IOException { if (identity.isLoggedIn()) { identity.logout(); } } private boolean isLogoutPath(PathConfiguration pathConfiguration) { return pathConfiguration != null && pathConfiguration.getLogoutConfiguration() != null; } private boolean isAuthorized(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response) { List<PathAuthorizer> authorizers = this.pathAuthorizers.get(pathConfiguration); if (authorizers != null) { for (PathAuthorizer authorizer : authorizers) { if (!authorizer.authorize(pathConfiguration, request, response)) { return false; } } } return true; } private void processRequest(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Continuing to process request for path [%s].", request.getRequestURI()); } chain.doFilter(request, response); } catch (Exception e) { throw new RuntimeException("Could not process request.", e); } } private void challengeClientForCredentials(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response) { HttpAuthenticationScheme authenticationScheme = getAuthenticationScheme(pathConfiguration, request); if (authenticationScheme != null) { if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Challenging client using authentication scheme [%s].", authenticationScheme); } try { authenticationScheme.challengeClient(request, response); } catch (Exception e) { throw new RuntimeException("Could not challenge client for credentials.", e); } HttpSession session = request.getSession(false); PathConfiguration authenticationOriginalPath; if (session != null) { authenticationOriginalPath = (PathConfiguration) session.getAttribute(AUTHENTICATION_ORIGINAL_PATH); if (authenticationOriginalPath == null || !authenticationOriginalPath.equals(pathConfiguration)) { session.setAttribute(AUTHENTICATION_ORIGINAL_PATH, pathConfiguration); } } } } private boolean performCORSAuthorizationIfRequired(PathConfiguration pathConfiguration, HttpServletRequest request, HttpServletResponse response) { if (pathConfiguration == null) { return true; } CORSConfiguration corsConfiguration = pathConfiguration.getCORSConfiguration(); if (corsConfiguration == null) { return true; } CORSRequestType type = CORSRequestType.detect(request); if (type.equals(CORSRequestType.ACTUAL)) { // Simple / actual CORS request CORS.handleActualRequest(corsConfiguration, request, response); } else if (type.equals(CORSRequestType.PREFLIGHT)) { // Preflight CORS request CORS.handlePreflightRequest(corsConfiguration, request, response); return false; } return true; } private void performAuthenticationIfRequired(PathConfiguration pathConfiguration, Identity identity, HttpServletRequest request, HttpServletResponse response) throws Exception { HttpAuthenticationScheme authenticationScheme = getAuthenticationScheme(pathConfiguration, request); if (authenticationScheme != null) { DefaultLoginCredentials creds = extractCredentials(request, authenticationScheme); if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Credentials extracted from request [%s]", creds.getCredential()); } if (creds.getCredential() != null) { if (identity.isLoggedIn()) { if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Forcing re-authentication. Logging out current user [%s]", identity.getAccount()); } identity.logout(); } creds = extractCredentials(request, authenticationScheme); } if (creds.getCredential() != null) { try { if (HTTP_LOGGER.isDebugEnabled()) { HTTP_LOGGER.debugf("Authenticating using credentials [%s]", creds.getCredential()); } identity.login(); authenticationScheme.onPostAuthentication(request, response); } catch (AuthenticationException ae) { HTTP_LOGGER.authenticationFailed(creds.getUserId(), ae); throw ae; } } } else { if (!identity.isLoggedIn()) { if (pathConfiguration != null && pathConfiguration.getAuthorizationConfiguration() != null) { throw new AuthenticationRequiredException("The given path [" + pathConfiguration.getUri() + "] requires authentication."); } } } } private HttpAuthenticationScheme getAuthenticationScheme(PathConfiguration pathConfiguration, HttpServletRequest request) { HttpAuthenticationScheme authenticationScheme = null; if (pathConfiguration != null) { AuthenticationConfiguration authcConfiguration = pathConfiguration.getAuthenticationConfiguration(); if (authcConfiguration != null) { AuthenticationSchemeConfiguration authSchemeConfiguration = authcConfiguration .getAuthenticationSchemeConfiguration(); authenticationScheme = this.authenticationSchemes.get(pathConfiguration); if (authenticationScheme == null) { Class<? extends HttpAuthenticationScheme> authcSchemeType; if (FormAuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { authcSchemeType = FormAuthenticationScheme.class; } else if (DigestAuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { authcSchemeType = DigestAuthenticationScheme.class; } else if (BasicAuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { authcSchemeType = BasicAuthenticationScheme.class; } else if (X509AuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { authcSchemeType = X509AuthenticationScheme.class; } else if (TokenAuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { authcSchemeType = TokenAuthenticationScheme.class; } else if (CustomAuthenticationConfiguration.class.isInstance(authSchemeConfiguration)) { CustomAuthenticationConfiguration customAuthcConfig = (CustomAuthenticationConfiguration) authSchemeConfiguration; authcSchemeType = customAuthcConfig.getSchemeType(); } else { throw new HttpSecurityConfigurationException("Unexpected Authentication Scheme configuration [" + authSchemeConfiguration + "]."); } authenticationScheme = resolveInstance(this.authenticationSchemesInstance, authcSchemeType); this.authenticationSchemes.put(pathConfiguration, authenticationScheme); } } } else { authenticationScheme = restorePreviousAuthenticationScheme(request); } return authenticationScheme; } @Override public void destroy() { } private HttpAuthenticationScheme restorePreviousAuthenticationScheme(HttpServletRequest request) { for (Map.Entry<PathConfiguration, HttpAuthenticationScheme> entry : this.authenticationSchemes.entrySet()) { DefaultLoginCredentials creds = extractCredentials(request, entry.getValue()); if (creds.getCredential() != null) { HttpSession session = request.getSession(false); if (session != null) { PathConfiguration originalAuthcPath = (PathConfiguration) session .getAttribute(AUTHENTICATION_ORIGINAL_PATH); if (originalAuthcPath != null && originalAuthcPath.equals(entry.getKey())) { session.removeAttribute(AUTHENTICATION_ORIGINAL_PATH); return entry.getValue(); } } } } return null; } private DefaultLoginCredentials extractCredentials(HttpServletRequest request, HttpAuthenticationScheme authenticationScheme) { DefaultLoginCredentials creds = getCredentials(); creds.invalidate(); authenticationScheme.extractCredential(request, creds); return creds; } private DefaultLoginCredentials getCredentials() { return resolveInstance(this.credentialsInstance); } private Identity getIdentity() { return resolveInstance(this.identityInstance); } private <I> I resolveInstance(Instance<I> instance) { return resolveInstance(instance, null); } private <I> I resolveInstance(Instance<I> fromInstance, Class<? extends I> type) { try { Instance<? extends I> instance; if (type != null) { instance = fromInstance.select(type); } else { instance = fromInstance; } if (instance.isUnsatisfied()) { throw new IllegalStateException("Instance [" + instance + "] not found."); } else if (instance.isAmbiguous()) { throw new IllegalStateException("Instance [" + instance + "] is ambiguous."); } return instance.get(); } catch (Exception e) { throw new IllegalStateException("Could not retrieve fromInstance [" + fromInstance + "].", e); } } private void initializeAuthenticationSchemes() { for (List<PathConfiguration> configurations : this.configuration.getPaths().values()) { for (PathConfiguration pathConfiguration : configurations) { if (pathConfiguration.isSecured()) { HttpAuthenticationScheme authenticationScheme = getAuthenticationScheme(pathConfiguration, null); if (authenticationScheme != null) { AuthenticationConfiguration authcConfig = pathConfiguration.getAuthenticationConfiguration(); AuthenticationSchemeConfiguration authcSchemeConfig = authcConfig .getAuthenticationSchemeConfiguration(); if (!CustomAuthenticationConfiguration.class.isInstance(authcSchemeConfig)) { try { authenticationScheme.initialize(authcSchemeConfig); } catch (Exception e) { throw new HttpSecurityConfigurationException( "Could not initialize Http Authentication Scheme [" + authenticationScheme + "].", e); } } } } } } } private void initializePathAuthorizers() { for (List<PathConfiguration> configurations : this.configuration.getPaths().values()) { for (PathConfiguration pathConfiguration : configurations) { if (pathConfiguration.isSecured()) { AuthorizationConfiguration authorizationConfiguration = pathConfiguration.getAuthorizationConfiguration(); if (authorizationConfiguration != null) { List<PathAuthorizer> pathAuthorizers = new ArrayList<PathAuthorizer>(); List<Class<? extends PathAuthorizer>> pathAuthorizerTypes = new ArrayList<Class<? extends PathAuthorizer>>( authorizationConfiguration.getAuthorizers()); pathAuthorizerTypes.addAll(getDefaultPathAuthorizers()); for (Class<? extends PathAuthorizer> authorizerType : pathAuthorizerTypes) { try { pathAuthorizers.add(resolveInstance(this.pathAuthorizerInstance, authorizerType)); } catch (Exception e) { throw new HttpSecurityConfigurationException("Could not resolve PathAuthorizer [" + authorizerType + "].", e); } } this.pathAuthorizers.put(pathConfiguration, pathAuthorizers); } } } } } private Set<Class<? extends PathAuthorizer>> getDefaultPathAuthorizers() { Set<Class<? extends PathAuthorizer>> defaultAuthorizers = new HashSet<Class<? extends PathAuthorizer>>(); defaultAuthorizers.add(RolePathAuthorizer.class); defaultAuthorizers.add(GroupPathAuthorizer.class); defaultAuthorizers.add(RealmPathAuthorizer.class); defaultAuthorizers.add(ExpressionPathAuthorizer.class); return defaultAuthorizers; } private void initializePathMatcher() { this.pathMatcher = new PathMatcher(this.configuration.getPaths(), this.elProcessor); } private PartitionManager getPartitionManager() { return resolveInstance(this.partitionManager); } private HttpServletRequest createRequestWrapper(HttpServletRequest request, Identity identity) { request = new PicketLinkHttpServletRequest(request, identity, getCredentials(), getPartitionManager(), this.elProcessor); this.servletRequestProducer.setRequest(request); return request; } }