AuthenticationFilter.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.portal.web.user;

import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

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 org.apache.commons.lang3.StringUtils;

import fr.paris.lutece.portal.business.securityheader.SecurityHeader;
import fr.paris.lutece.portal.business.securityheader.SecurityHeaderPageCategory;
import fr.paris.lutece.portal.business.securityheader.SecurityHeaderType;
import fr.paris.lutece.portal.service.admin.AccessDeniedException;
import fr.paris.lutece.portal.service.admin.AdminAuthenticationService;
import fr.paris.lutece.portal.service.admin.AdminUserService;
import fr.paris.lutece.portal.service.admin.PasswordResetException;
import fr.paris.lutece.portal.service.message.AdminMessage;
import fr.paris.lutece.portal.service.message.AdminMessageService;
import fr.paris.lutece.portal.service.security.SecurityTokenService;
import fr.paris.lutece.portal.service.security.UserNotSignedException;
import fr.paris.lutece.portal.service.securityheader.SecurityHeaderService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.web.constants.Messages;
import fr.paris.lutece.portal.web.constants.Parameters;
import fr.paris.lutece.util.url.UrlItem;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Filter to prevent unauthenticated access to admin
 */
public class AuthenticationFilter implements Filter
{
    private static final String PROPERTY_URL_PREFIX = "path.jsp.admin.public.";
    private static final String PROPERTY_URL_SUFFIX_LIST = "list";
    private static final String CONSTANT_LIST_SEPARATOR = ",";
    private static final String PROPERTY_RESET_EXCEPTION_MESSAGE = "User must reset his password.";
    private static final String PROPERTY_JSP_URL_ADMIN_LOGOUT = "lutece.admin.logout.url";
    private static final String JSP_URL_ADMIN_LOGIN = "jsp/admin/AdminLogin.jsp";
    private static final String BEAN_SECURITY_HEADER_SERVICE = "securityHeaderService";
    private static final String LOGGER_LUTECE_SECURITY_HEADER = "lutece.securityHeader";
    private Logger _logger = LogManager.getLogger( LOGGER_LUTECE_SECURITY_HEADER );

    /**
     * {@inheritDoc}
     */
    @Override
    public void init( FilterConfig config ) throws ServletException
    {
        // Do nothing
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void destroy( )
    {
        // Do nothing
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException
    {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        AppLogService.debug( "Accessing url : {}", ( ) -> getResquestedUrl( req ) );

        if ( isPrivateUrl( req ) )
        {
        	addAdminAuthenticatedPagesHeaders(req, resp);
            try
            {
                filterAccess( req );
            }
            catch( UserNotSignedException e )
            {
                AdminAuthenticationService.getInstance( ).setLoginNextUrl( req );

                String strRedirectUrl = null;

                if ( AdminAuthenticationService.getInstance( ).isExternalAuthentication( ) )
                {
                    AppLogService.debug( "New session behind external authentication : {}", ( ) -> getResquestedUrl( req ) );

                    strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_NEW_SESSION, getRedirectUrlExternalAuthentication( req ),
                            AdminMessage.TYPE_INFO );
                }
                else
                {
                    AppLogService.debug( "Access NOT granted to url : {}", ( ) -> getResquestedUrl( req ) );

                    strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_NOT_AUTHENTICATED, getRedirectUrl( req ),
                            AdminMessage.TYPE_WARNING );
                }

                resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );

                return;
            }
            catch( AccessDeniedException e )
            {
                AppLogService.debug( "Access NOT granted to url : {}", getResquestedUrl( req ) );

                String strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_AUTH_FAILURE, getRedirectUrl( req ), AdminMessage.TYPE_ERROR );
                resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );

                return;
            }
            catch( PasswordResetException e )
            {
                if ( !getResquestedUrl( req ).equals( getChangePasswordUrl( req ) ) && !getResquestedUrl( req ).equals( getLoginUrl( req ) ) )
                {
                    String strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_MUST_CHANGE_PASSWORD, getChangePasswordUrl( req ),
                            AdminMessage.TYPE_ERROR );
                    resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );

                    return;
                }
            }
        }
        else if(isAdminLogoutUrl(req))
        {
        	addBoLogoutPageSecurityHeaders(req, resp);	
        }

        chain.doFilter( request, response );
    }

    /**
     * Adds http security headers to BO admin authenticated pages.
     * 
     * @param request
     *            the http request
     * @param response
     *            the http response
     */
    private void addAdminAuthenticatedPagesHeaders( HttpServletRequest request, HttpServletResponse response )
    {
    	SecurityHeaderService securityHeaderService = SpringContextService.getBean( BEAN_SECURITY_HEADER_SERVICE );
    	Collection<SecurityHeader> securityHeadersToAddList = securityHeaderService.findActive( SecurityHeaderType.PAGE.getCode( ), SecurityHeaderPageCategory.AUTHENTICATED_ADMIN_BACK_OFFICE.getCode( ) );
    	if( securityHeadersToAddList != null )
    	{
    		for( SecurityHeader securityHeader : securityHeadersToAddList )
        	{
        		response.setHeader( securityHeader.getName( ), securityHeader.getValue( ) );
        		_logger.debug( "Security header added to admin authenticated BO page {} - name : {}, value : {} ", request.getServletPath( ), securityHeader.getName( ), securityHeader.getValue( ) );
        	}
    	}   	
    }
    
    /**
     * Adds http security headers to BO admin logout page.
     * 
     * @param request
     *            the http request
     * @param response
     *            the http response
     */
    private void addBoLogoutPageSecurityHeaders( HttpServletRequest request, HttpServletResponse response )
    {
    	SecurityHeaderService securityHeaderService = SpringContextService.getBean( BEAN_SECURITY_HEADER_SERVICE );
    	Collection<SecurityHeader> securityHeadersList = securityHeaderService.findActive( SecurityHeaderType.PAGE.getCode( ), SecurityHeaderPageCategory.LOGOUT_BO.getCode( ) );
    	if( securityHeadersList != null )
    	{
    		for( SecurityHeader securityHeader : securityHeadersList )
        	{
        		response.setHeader( securityHeader.getName( ), securityHeader.getValue( ) );
        		_logger.debug( "Security header added to logout page {} - name : {}, value : {} ", request.getServletPath( ), securityHeader.getName( ), securityHeader.getValue( ) );
        	}
    	}   	
    }
    
     /**
      * Checks if url of the request corresponds to the BO admin logout url.
      * 
      * @param request 
      *            the request
      * @return
      */
     private boolean isAdminLogoutUrl( HttpServletRequest request )
     {
	     return getResquestedUrl( request ).equals( getAbsoluteUrl( request, AppPropertiesService.getProperty( PROPERTY_JSP_URL_ADMIN_LOGOUT ) ) );
     }
    
    /**
     * Build the url to redirect to if not logged. This is actually the login page of the authentication module, completed with the request parameters.
     * 
     * @param request
     *            the http request
     * @return the string representation of the redirection url - absolute - with request parameters.
     */
    private String getRedirectUrl( HttpServletRequest request )
    {
        String strLoginUrl = getLoginUrl( request );

        if ( strLoginUrl == null )
        {
            return null;
        }

        UrlItem url = new UrlItem( strLoginUrl );

        return url.getUrl( );
    }

    /**
     * Get the absolute login url
     *
     * @param request
     *            the http request
     * @return the login url, in its absolute form
     *
     */
    private String getLoginUrl( HttpServletRequest request )
    {
        String strLoginUrl = AdminAuthenticationService.getInstance( ).getLoginPageUrl( );

        return getAbsoluteUrl( request, strLoginUrl );
    }

    /**
     * Gets the logout url.
     *
     * @param request
     *            the request
     * @return the logout url
     */
    private String getLogoutUrl( HttpServletRequest request )
    {
        return getAbsoluteUrl( request, AppPropertiesService.getProperty( PROPERTY_JSP_URL_ADMIN_LOGOUT ) );
    }

    /**
     * Get the absolute login url
     *
     * @param request
     *            the http request
     * @return the login url, in its absolute form
     *
     */
    private String getChangePasswordUrl( HttpServletRequest request )
    {
        String strChangePasswordUrl = AdminAuthenticationService.getInstance( ).getChangePasswordPageUrl( );

        return getAbsoluteUrl( request, strChangePasswordUrl );
    }

    /**
     * Check wether a given url is to be considered as private (ie that needs a successful authentication to be accessed) or public (ie that can be access
     * without being authenticated)
     * 
     * @param request
     *            the http request
     * @return true if the url needs to be authenticated, false otherwise
     *
     */
    private boolean isPrivateUrl( HttpServletRequest request )
    {
        String strUrl = getResquestedUrl( request );
        Set<String> allowedUrlSet = createAllowedUrlSet( request );
        return !allowedUrlSet.contains( strUrl ) && !isInPublicUrlList( request, strUrl );
    }

    private Set<String> createAllowedUrlSet( HttpServletRequest request )
    {
        Set<String> set = new HashSet<>( );
        set.add( getAbsoluteUrl( request, JSP_URL_ADMIN_LOGIN ) );
        set.add( getLoginUrl( request ) );
        set.add( getLogoutUrl( request ) );
        return set;
    }

    /**
     * check that the access is granted
     * 
     * @param request
     *            The HTTP request
     * @throws AccessDeniedException
     *             If the user is not allowed
     * @throws UserNotSignedException
     *             If the user is not signed
     *
     **/
    private static void filterAccess( HttpServletRequest request ) throws UserNotSignedException, AccessDeniedException
    {
        if ( AdminAuthenticationService.getInstance( ).isExternalAuthentication( ) )
        {
            // The authentication is external
            // Should register the user if it's not already done
            AdminAuthenticationService.getInstance( ).getRemoteUser( request );
        }
        else
        {
            if ( AdminAuthenticationService.getInstance( ).getRegisteredUser( request ) == null )
            {
                // Authentication is required to access to the admin
                throw new UserNotSignedException( );
            }

            if ( AdminUserService.getAdminUser( request ).isPasswordReset( ) )
            {
                throw new PasswordResetException( PROPERTY_RESET_EXCEPTION_MESSAGE );
            }
        }
    }

    /**
     * Checks if the requested is in the list of urls that are under jsp/admin but shouldn't be protected
     * 
     * @param request
     *            the http request (provides the base path if needed)
     * @param strRequestedUrl
     *            the url to test : it should start with "http://" is absolute, or should be relative to the webapp root otherwise
     * @return true if the url is in the list, false otherwise
     *
     */
    private boolean isInPublicUrlList( HttpServletRequest request, String strRequestedUrl )
    {
        // recovers list from the
        String strList = AppPropertiesService.getProperty( PROPERTY_URL_PREFIX + PROPERTY_URL_SUFFIX_LIST );

        // extracts each item (separated by a comma) from the includes list
        StringTokenizer strTokens = new StringTokenizer( strList, CONSTANT_LIST_SEPARATOR );

        while ( strTokens.hasMoreTokens( ) )
        {
            String strName = strTokens.nextToken( );
            String strUrl = AppPropertiesService.getProperty( PROPERTY_URL_PREFIX + strName );
            strUrl = getAbsoluteUrl( request, strUrl );

            if ( strRequestedUrl.equals( strUrl ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the absolute url corresponding to the given one, if the later was found to be relative. An url starting with "http://" is absolute. A relative
     * url should be given relatively to the webapp root.
     * 
     * @param request
     *            the http request (provides the base path if needed)
     * @param strUrl
     *            the url to transform
     * @return the corresponding absolute url
     *
     */
    private String getAbsoluteUrl( HttpServletRequest request, String strUrl )
    {
        if ( ( strUrl != null ) && !strUrl.startsWith( "http://" ) && !strUrl.startsWith( "https://" ) )
        {
            return AppPathService.getBaseUrl( request ) + strUrl;
        }

        return strUrl;
    }

    /**
     * Return the absolute representation of the requested url
     * 
     * @param request
     *            the http request (provides the base path if needed)
     * @return the requested url has a string
     *
     */
    private String getResquestedUrl( HttpServletRequest request )
    {
        return AppPathService.getBaseUrl( request ) + request.getServletPath( ).substring( 1 );
    }

    /**
     * Build the url to redirect after opening a new session when using external admin authentication. This is actually the requested url if provided; else the
     * admin autentication admin menu.
     * 
     * @param request
     *            the http request
     * @return the string representation of the redirection url - absolute - when using external admin authentication.
     */
    private String getRedirectUrlExternalAuthentication( HttpServletRequest request )
    {
        String strNextUrl = AdminAuthenticationService.getInstance( ).getLoginNextUrl( request );

        if ( StringUtils.isEmpty( strNextUrl ) )
        {
            strNextUrl = AppPathService.getAdminMenuUrl( );
        }

        return strNextUrl;
    }
}