View Javadoc
1   /*
2    * Copyright (c) 2002-2025, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.web.user;
35  
36  import java.io.IOException;
37  import java.util.Collection;
38  import java.util.Enumeration;
39  import java.util.HashSet;
40  import java.util.Set;
41  import java.util.StringTokenizer;
42  
43  import javax.servlet.Filter;
44  import javax.servlet.FilterChain;
45  import javax.servlet.FilterConfig;
46  import javax.servlet.ServletException;
47  import javax.servlet.ServletRequest;
48  import javax.servlet.ServletResponse;
49  import javax.servlet.http.HttpServletRequest;
50  import javax.servlet.http.HttpServletResponse;
51  
52  import org.apache.commons.lang3.StringUtils;
53  
54  import fr.paris.lutece.portal.business.securityheader.SecurityHeader;
55  import fr.paris.lutece.portal.business.securityheader.SecurityHeaderPageCategory;
56  import fr.paris.lutece.portal.business.securityheader.SecurityHeaderType;
57  import fr.paris.lutece.portal.service.admin.AccessDeniedException;
58  import fr.paris.lutece.portal.service.admin.AdminAuthenticationService;
59  import fr.paris.lutece.portal.service.admin.AdminUserService;
60  import fr.paris.lutece.portal.service.admin.PasswordResetException;
61  import fr.paris.lutece.portal.service.message.AdminMessage;
62  import fr.paris.lutece.portal.service.message.AdminMessageService;
63  import fr.paris.lutece.portal.service.security.SecurityTokenService;
64  import fr.paris.lutece.portal.service.security.UserNotSignedException;
65  import fr.paris.lutece.portal.service.securityheader.SecurityHeaderService;
66  import fr.paris.lutece.portal.service.spring.SpringContextService;
67  import fr.paris.lutece.portal.service.util.AppLogService;
68  import fr.paris.lutece.portal.service.util.AppPathService;
69  import fr.paris.lutece.portal.service.util.AppPropertiesService;
70  import fr.paris.lutece.portal.web.constants.Messages;
71  import fr.paris.lutece.portal.web.constants.Parameters;
72  import fr.paris.lutece.util.url.UrlItem;
73  
74  import org.apache.logging.log4j.LogManager;
75  import org.apache.logging.log4j.Logger;
76  
77  /**
78   * Filter to prevent unauthenticated access to admin
79   */
80  public class AuthenticationFilter implements Filter
81  {
82      private static final String PROPERTY_URL_PREFIX = "path.jsp.admin.public.";
83      private static final String PROPERTY_URL_SUFFIX_LIST = "list";
84      private static final String CONSTANT_LIST_SEPARATOR = ",";
85      private static final String PROPERTY_RESET_EXCEPTION_MESSAGE = "User must reset his password.";
86      private static final String PROPERTY_JSP_URL_ADMIN_LOGOUT = "lutece.admin.logout.url";
87      private static final String JSP_URL_ADMIN_LOGIN = "jsp/admin/AdminLogin.jsp";
88      private static final String BEAN_SECURITY_HEADER_SERVICE = "securityHeaderService";
89      private static final String LOGGER_LUTECE_SECURITY_HEADER = "lutece.securityHeader";
90      private Logger _logger = LogManager.getLogger( LOGGER_LUTECE_SECURITY_HEADER );
91  
92      /**
93       * {@inheritDoc}
94       */
95      @Override
96      public void init( FilterConfig config ) throws ServletException
97      {
98          // Do nothing
99      }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
105     public void destroy( )
106     {
107         // Do nothing
108     }
109 
110     /**
111      * {@inheritDoc}
112      */
113     @Override
114     public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException
115     {
116         HttpServletRequest req = (HttpServletRequest) request;
117         HttpServletResponse resp = (HttpServletResponse) response;
118 
119         AppLogService.debug( "Accessing url : {}", ( ) -> getResquestedUrl( req ) );
120 
121         if ( isPrivateUrl( req ) )
122         {
123         	addAdminAuthenticatedPagesHeaders(req, resp);
124             try
125             {
126                 filterAccess( req );
127             }
128             catch( UserNotSignedException e )
129             {
130                 AdminAuthenticationService.getInstance( ).setLoginNextUrl( req );
131 
132                 String strRedirectUrl = null;
133 
134                 if ( AdminAuthenticationService.getInstance( ).isExternalAuthentication( ) )
135                 {
136                     AppLogService.debug( "New session behind external authentication : {}", ( ) -> getResquestedUrl( req ) );
137 
138                     strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_NEW_SESSION, getRedirectUrlExternalAuthentication( req ),
139                             AdminMessage.TYPE_INFO );
140                 }
141                 else
142                 {
143                     AppLogService.debug( "Access NOT granted to url : {}", ( ) -> getResquestedUrl( req ) );
144 
145                     strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_NOT_AUTHENTICATED, getRedirectUrl( req ),
146                             AdminMessage.TYPE_WARNING );
147                 }
148 
149                 resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );
150 
151                 return;
152             }
153             catch( AccessDeniedException e )
154             {
155                 AppLogService.debug( "Access NOT granted to url : {}", getResquestedUrl( req ) );
156 
157                 String strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_AUTH_FAILURE, getRedirectUrl( req ), AdminMessage.TYPE_ERROR );
158                 resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );
159 
160                 return;
161             }
162             catch( PasswordResetException e )
163             {
164                 if ( !getResquestedUrl( req ).equals( getChangePasswordUrl( req ) ) && !getResquestedUrl( req ).equals( getLoginUrl( req ) ) )
165                 {
166                     String strRedirectUrl = AdminMessageService.getMessageUrl( req, Messages.MESSAGE_USER_MUST_CHANGE_PASSWORD, getChangePasswordUrl( req ),
167                             AdminMessage.TYPE_ERROR );
168                     resp.sendRedirect( getAbsoluteUrl( req, strRedirectUrl ) );
169 
170                     return;
171                 }
172             }
173         }
174         else if(isAdminLogoutUrl(req))
175         {
176         	addBoLogoutPageSecurityHeaders(req, resp);	
177         }
178 
179         chain.doFilter( request, response );
180     }
181 
182     /**
183      * Adds http security headers to BO admin authenticated pages.
184      * 
185      * @param request
186      *            the http request
187      * @param response
188      *            the http response
189      */
190     private void addAdminAuthenticatedPagesHeaders( HttpServletRequest request, HttpServletResponse response )
191     {
192     	SecurityHeaderService securityHeaderService = SpringContextService.getBean( BEAN_SECURITY_HEADER_SERVICE );
193     	Collection<SecurityHeader> securityHeadersToAddList = securityHeaderService.findActive( SecurityHeaderType.PAGE.getCode( ), SecurityHeaderPageCategory.AUTHENTICATED_ADMIN_BACK_OFFICE.getCode( ) );
194     	if( securityHeadersToAddList != null )
195     	{
196     		for( SecurityHeader securityHeader : securityHeadersToAddList )
197         	{
198         		response.setHeader( securityHeader.getName( ), securityHeader.getValue( ) );
199         		_logger.debug( "Security header added to admin authenticated BO page {} - name : {}, value : {} ", request.getServletPath( ), securityHeader.getName( ), securityHeader.getValue( ) );
200         	}
201     	}   	
202     }
203     
204     /**
205      * Adds http security headers to BO admin logout page.
206      * 
207      * @param request
208      *            the http request
209      * @param response
210      *            the http response
211      */
212     private void addBoLogoutPageSecurityHeaders( HttpServletRequest request, HttpServletResponse response )
213     {
214     	SecurityHeaderService securityHeaderService = SpringContextService.getBean( BEAN_SECURITY_HEADER_SERVICE );
215     	Collection<SecurityHeader> securityHeadersList = securityHeaderService.findActive( SecurityHeaderType.PAGE.getCode( ), SecurityHeaderPageCategory.LOGOUT_BO.getCode( ) );
216     	if( securityHeadersList != null )
217     	{
218     		for( SecurityHeader securityHeader : securityHeadersList )
219         	{
220         		response.setHeader( securityHeader.getName( ), securityHeader.getValue( ) );
221         		_logger.debug( "Security header added to logout page {} - name : {}, value : {} ", request.getServletPath( ), securityHeader.getName( ), securityHeader.getValue( ) );
222         	}
223     	}   	
224     }
225     
226      /**
227       * Checks if url of the request corresponds to the BO admin logout url.
228       * 
229       * @param request 
230       *            the request
231       * @return
232       */
233      private boolean isAdminLogoutUrl( HttpServletRequest request )
234      {
235 	     return getResquestedUrl( request ).equals( getAbsoluteUrl( request, AppPropertiesService.getProperty( PROPERTY_JSP_URL_ADMIN_LOGOUT ) ) );
236      }
237     
238     /**
239      * Build the url to redirect to if not logged. This is actually the login page of the authentication module, completed with the request parameters.
240      * 
241      * @param request
242      *            the http request
243      * @return the string representation of the redirection url - absolute - with request parameters.
244      */
245     private String getRedirectUrl( HttpServletRequest request )
246     {
247         String strLoginUrl = getLoginUrl( request );
248 
249         if ( strLoginUrl == null )
250         {
251             return null;
252         }
253 
254         UrlItem/url/UrlItem.html#UrlItem">UrlItem url = new UrlItem( strLoginUrl );
255 
256         return url.getUrl( );
257     }
258 
259     /**
260      * Get the absolute login url
261      *
262      * @param request
263      *            the http request
264      * @return the login url, in its absolute form
265      *
266      */
267     private String getLoginUrl( HttpServletRequest request )
268     {
269         String strLoginUrl = AdminAuthenticationService.getInstance( ).getLoginPageUrl( );
270 
271         return getAbsoluteUrl( request, strLoginUrl );
272     }
273 
274     /**
275      * Gets the logout url.
276      *
277      * @param request
278      *            the request
279      * @return the logout url
280      */
281     private String getLogoutUrl( HttpServletRequest request )
282     {
283         return getAbsoluteUrl( request, AppPropertiesService.getProperty( PROPERTY_JSP_URL_ADMIN_LOGOUT ) );
284     }
285 
286     /**
287      * Get the absolute login url
288      *
289      * @param request
290      *            the http request
291      * @return the login url, in its absolute form
292      *
293      */
294     private String getChangePasswordUrl( HttpServletRequest request )
295     {
296         String strChangePasswordUrl = AdminAuthenticationService.getInstance( ).getChangePasswordPageUrl( );
297 
298         return getAbsoluteUrl( request, strChangePasswordUrl );
299     }
300 
301     /**
302      * 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
303      * without being authenticated)
304      * 
305      * @param request
306      *            the http request
307      * @return true if the url needs to be authenticated, false otherwise
308      *
309      */
310     private boolean isPrivateUrl( HttpServletRequest request )
311     {
312         String strUrl = getResquestedUrl( request );
313         Set<String> allowedUrlSet = createAllowedUrlSet( request );
314         return !allowedUrlSet.contains( strUrl ) && !isInPublicUrlList( request, strUrl );
315     }
316 
317     private Set<String> createAllowedUrlSet( HttpServletRequest request )
318     {
319         Set<String> set = new HashSet<>( );
320         set.add( getAbsoluteUrl( request, JSP_URL_ADMIN_LOGIN ) );
321         set.add( getLoginUrl( request ) );
322         set.add( getLogoutUrl( request ) );
323         return set;
324     }
325 
326     /**
327      * check that the access is granted
328      * 
329      * @param request
330      *            The HTTP request
331      * @throws AccessDeniedException
332      *             If the user is not allowed
333      * @throws UserNotSignedException
334      *             If the user is not signed
335      *
336      **/
337     private static void filterAccess( HttpServletRequest request ) throws UserNotSignedException, AccessDeniedException
338     {
339         if ( AdminAuthenticationService.getInstance( ).isExternalAuthentication( ) )
340         {
341             // The authentication is external
342             // Should register the user if it's not already done
343             AdminAuthenticationService.getInstance( ).getRemoteUser( request );
344         }
345         else
346         {
347             if ( AdminAuthenticationService.getInstance( ).getRegisteredUser( request ) == null )
348             {
349                 // Authentication is required to access to the admin
350                 throw new UserNotSignedException( );
351             }
352 
353             if ( AdminUserService.getAdminUser( request ).isPasswordReset( ) )
354             {
355                 throw new PasswordResetException( PROPERTY_RESET_EXCEPTION_MESSAGE );
356             }
357         }
358     }
359 
360     /**
361      * Checks if the requested is in the list of urls that are under jsp/admin but shouldn't be protected
362      * 
363      * @param request
364      *            the http request (provides the base path if needed)
365      * @param strRequestedUrl
366      *            the url to test : it should start with "http://" is absolute, or should be relative to the webapp root otherwise
367      * @return true if the url is in the list, false otherwise
368      *
369      */
370     private boolean isInPublicUrlList( HttpServletRequest request, String strRequestedUrl )
371     {
372         // recovers list from the
373         String strList = AppPropertiesService.getProperty( PROPERTY_URL_PREFIX + PROPERTY_URL_SUFFIX_LIST );
374 
375         // extracts each item (separated by a comma) from the includes list
376         StringTokenizer strTokens = new StringTokenizer( strList, CONSTANT_LIST_SEPARATOR );
377 
378         while ( strTokens.hasMoreTokens( ) )
379         {
380             String strName = strTokens.nextToken( );
381             String strUrl = AppPropertiesService.getProperty( PROPERTY_URL_PREFIX + strName );
382             strUrl = getAbsoluteUrl( request, strUrl );
383 
384             if ( strRequestedUrl.equals( strUrl ) )
385             {
386                 return true;
387             }
388         }
389 
390         return false;
391     }
392 
393     /**
394      * 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
395      * url should be given relatively to the webapp root.
396      * 
397      * @param request
398      *            the http request (provides the base path if needed)
399      * @param strUrl
400      *            the url to transform
401      * @return the corresponding absolute url
402      *
403      */
404     private String getAbsoluteUrl( HttpServletRequest request, String strUrl )
405     {
406         if ( ( strUrl != null ) && !strUrl.startsWith( "http://" ) && !strUrl.startsWith( "https://" ) )
407         {
408             return AppPathService.getBaseUrl( request ) + strUrl;
409         }
410 
411         return strUrl;
412     }
413 
414     /**
415      * Return the absolute representation of the requested url
416      * 
417      * @param request
418      *            the http request (provides the base path if needed)
419      * @return the requested url has a string
420      *
421      */
422     private String getResquestedUrl( HttpServletRequest request )
423     {
424         return AppPathService.getBaseUrl( request ) + request.getServletPath( ).substring( 1 );
425     }
426 
427     /**
428      * 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
429      * admin autentication admin menu.
430      * 
431      * @param request
432      *            the http request
433      * @return the string representation of the redirection url - absolute - when using external admin authentication.
434      */
435     private String getRedirectUrlExternalAuthentication( HttpServletRequest request )
436     {
437         String strNextUrl = AdminAuthenticationService.getInstance( ).getLoginNextUrl( request );
438 
439         if ( StringUtils.isEmpty( strNextUrl ) )
440         {
441             strNextUrl = AppPathService.getAdminMenuUrl( );
442         }
443 
444         return strNextUrl;
445     }
446 }