001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.authentication;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Enumeration;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.Optional;
029import java.util.Set;
030import java.util.regex.Pattern;
031import java.util.stream.Collectors;
032
033import org.apache.avalon.framework.activity.Initializable;
034import org.apache.avalon.framework.parameters.Parameters;
035import org.apache.avalon.framework.thread.ThreadSafe;
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.acting.ServiceableAction;
038import org.apache.cocoon.environment.ObjectModelHelper;
039import org.apache.cocoon.environment.Redirector;
040import org.apache.cocoon.environment.Request;
041import org.apache.cocoon.environment.Session;
042import org.apache.cocoon.environment.SourceResolver;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.Strings;
045
046import org.ametys.core.ObservationConstants;
047import org.ametys.core.authentication.token.AuthenticationTokenManager;
048import org.ametys.core.observation.Event;
049import org.ametys.core.observation.ObservationManager;
050import org.ametys.core.trace.ForensicLogger;
051import org.ametys.core.user.CurrentUserProvider;
052import org.ametys.core.user.User;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.core.user.UserManager;
055import org.ametys.core.user.directory.ModifiableUserDirectory;
056import org.ametys.core.user.directory.WeakPasswordException;
057import org.ametys.core.user.population.PopulationContextHelper;
058import org.ametys.core.user.population.UserPopulation;
059import org.ametys.core.user.population.UserPopulationDAO;
060import org.ametys.core.user.status.UserStatusManager;
061import org.ametys.core.util.URIUtils;
062import org.ametys.plugins.core.impl.authentication.FormCredentialProvider;
063import org.ametys.plugins.core.user.UserDAO;
064import org.ametys.plugins.core.user.management.UserPasswordManager;
065import org.ametys.runtime.authentication.AccessDeniedException;
066import org.ametys.runtime.authentication.AuthorizationRequiredException;
067import org.ametys.runtime.maintenance.MaintenanceAction;
068import org.ametys.runtime.servlet.RuntimeServlet;
069import org.ametys.runtime.servlet.RuntimeServlet.RunMode;
070import org.ametys.runtime.workspace.WorkspaceMatcher;
071
072/**
073 * Cocoon action to perform authentication.<br>
074 * The {@link CredentialProvider} define the authentication method and retrieves {@link Credentials}.<br>
075 * Finally, the Users instance extract the Principal corresponding to the {@link Credentials}.
076 */
077public class AuthenticateAction extends ServiceableAction implements ThreadSafe, Initializable
078{
079    /** The request attribute to allow internal action from an internal request. */
080    public static final String REQUEST_ATTRIBUTE_INTERNAL_ALLOWED = "Runtime:InternalAllowedRequest";
081    
082    /** The request attribute meaning that the request was not authenticated but granted */
083    public static final String REQUEST_ATTRIBUTE_GRANTED = "Runtime:GrantedRequest";
084    /** The request attribute name for transmitting the list of user populations */
085    public static final String REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST = "Runtime:UserPopulationsList";
086    /** The request attribute name for transmitting the currently chosen user population */
087    public static final String REQUEST_ATTRIBUTE_USER_POPULATION_ID = "Runtime:CurrentUserPopulationId";
088    /** The request attribute name for transmitting the login page url */
089    public static final String REQUEST_ATTRIBUTE_LOGIN_URL = "Runtime:RequestLoginURL";
090    
091    /** The session attribute name for storing the identity of the connected user */
092    public static final String SESSION_USERIDENTITY = "Runtime:UserIdentity";
093
094    /** Name of the user population HTML field */
095    public static final String REQUEST_PARAMETER_POPULATION_NAME = "UserPopulation";
096    /** Name of the credential provider index HTML field */
097    public static final String REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX = "CredentialProviderIndex";
098    /** Name of a parameter to change non blocking CP behavior */
099    public static final String REQUEST_PARAMETER_NONBLOCING = "NonBlocking";
100    
101    /** The request attribute name for indicating that the authentication process has been made. */
102    public static final String REQUEST_ATTRIBUTE_AUTHENTICATED = "Runtime:RequestAuthenticated";
103
104    /** The request parameter holding the token */
105    public static final String REQUEST_PARAMETER_TOKEN = "token";
106    /** The header parameter that can be set to handle the token */
107    public static final String HEADER_TOKEN = "X-Ametys-Token";
108
109    /** The sitemap parameter holding the token */
110    protected static final String PARAMETERS_PARAMETER_TOKEN = "token";
111    /** The sitemap parameter holding the token context */
112    protected static final String PARAMETERS_PARAMETER_TOKEN_CONTEXT = "tokenContext";
113    /** The request attribute name for transmitting a boolean that tell if there is a list of credential provider to choose */
114    protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST = "Runtime:RequestListCredentialProvider";
115    /** The request attribute name for transmitting the index in the list of chosen credential provider */
116    protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX = "Runtime:RequestCredentialProviderIndex";
117    /** The request attribute name to know if user population list should be proposed */
118    protected static final String REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST = "Runtime:UserPopulationsListDisplay";
119    /** The request attribute name for transmitting the potential list of user populations to the login screen . */
120    protected static final String REQUEST_ATTRIBUTE_INVALID_POPULATION = "Runtime:RequestInvalidPopulation";
121    /** The request attribute name for transmitting the list of contexts */
122    protected static final String REQUEST_ATTRIBUTE_CONTEXTS = "Runtime:Contexts";
123
124    /** The session attribute name for storing the credential provider index of the authentication (during connection process) */
125    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX = "Runtime:ConnectingCredentialProviderIndex";
126    /** The session attribute name for storing the last known credential provider index of the authentication (during connection process)*/
127    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN = "Runtime:ConnectingCredentialProviderIndexLastKnown";
128    /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=&gt;false, blocking=&gt;true (during connection process) */
129    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_MODE = "Runtime:ConnectingCredentialProviderMode";
130    /** The session attribute name for storing the id of the user population (during connection process) */
131    protected static final String SESSION_CONNECTING_USERPOPULATION_ID = "Runtime:ConnectingUserPopulationId";
132    
133    /** The session attribute name for storing the credential provider of the authentication */
134    protected static final String SESSION_CREDENTIALPROVIDER = "Runtime:CredentialProvider";
135    /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=&gt;false, blocking=&gt;true */
136    protected static final String SESSION_CREDENTIALPROVIDER_MODE = "Runtime:CredentialProviderMode";
137
138    /** The sitemap parameter to set the token mode of the action */
139    protected static final String SITEMAP_PARAMETER_TOKEN_MODE = "token-mode";
140    
141    /** The DAO for user populations */
142    protected UserPopulationDAO _userPopulationDAO;
143    /** The user manager */
144    protected UserManager _userManager;
145    /** The helper for the associations population/context */
146    protected PopulationContextHelper _populationContextHelper;
147    /** The current user provider */
148    protected CurrentUserProvider _currentUserProvider;
149    
150    /** url requires for authentication */
151    protected Collection<Pattern> _acceptedUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/core/authenticate/[0-9]+$"), Pattern.compile("^plugins/core/reset-password.html$")});
152
153    /** The authentication token manager */
154    protected AuthenticationTokenManager _authenticateTokenManager;
155    /** The observation manager */
156    protected ObservationManager _observationManager;
157    /** The user account manager */
158    protected UserPasswordManager _userPasswordManager;
159    /** The user status manager */
160    protected UserStatusManager _userStatusManager;
161
162    /**
163     * The token mode of this authentication action
164     */
165    protected enum TOKEN_MODE
166    {
167        /** In this mode, only the token will be taken in account. If no token is found, authentication will not be considered done */
168        TOKEN_ONLY,
169        /** In this mode, the token will be taken in account but if no token is found, user will be considered as anonymous and authentication will be considered done */
170        ALLOW_ANONYMOUS,
171        /** In this default mode, the token will be taken in account, but if no token is found, the authentication process will continue */
172        DEFAULT
173    }
174    
175    @Override
176    public void initialize() throws Exception
177    {
178        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
179        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
180        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
181        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
182        
183        // These component are not safe but the action is
184        if (manager.hasService(UserPasswordManager.ROLE))
185        {
186            _userPasswordManager = (UserPasswordManager) manager.lookup(UserPasswordManager.ROLE);
187            _userStatusManager = (UserStatusManager) manager.lookup(UserStatusManager.ROLE);
188            _authenticateTokenManager = (AuthenticationTokenManager) manager.lookup(AuthenticationTokenManager.ROLE);
189            _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
190        }
191    }
192    
193    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
194    {
195        Request request = ObjectModelHelper.getRequest(objectModel);
196        
197        if (_preFlightCheck(redirector, resolver, objectModel, source, parameters) || _handleAuthenticationToken(request, parameters))
198        {
199            // We passed the authentication, let's mark it now
200            request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
201            
202            // We passed the authentication (with a user)
203            return EMPTY_MAP;
204        }
205        
206        // At this point, the user is still anonymous
207        
208        // If only token are authorized for authentication, stop authentication process. There is no user authenticated here.
209        if (_getTokenMode(parameters) != TOKEN_MODE.DEFAULT)
210        {
211            if (_getTokenMode(parameters) == TOKEN_MODE.ALLOW_ANONYMOUS)
212            {
213                request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
214            }
215            return null;
216        }
217        
218        // At this point, we already know that the entire process will be executed, whatever the outcome
219        // Set the flag, so that the authentication process won't repeat
220        request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
221        
222        // Get population and if possible credential providers
223        List<UserPopulation> chosenUserPopulations = new ArrayList<>();
224        List<CredentialProvider> credentialProviders = new ArrayList<>();
225        if (!_prepareUserPopulationsAndCredentialProviders(request, parameters, redirector, chosenUserPopulations, credentialProviders))
226        {
227            // Let's display the population screen
228            return EMPTY_MAP;
229        }
230        
231        // Get the currently running credential provider
232        int runningCredentialProviderIndex = _getCurrentCredentialProviderIndex(request, credentialProviders);
233        request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX, runningCredentialProviderIndex);
234        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_URL, getLoginURL(request));
235        
236        // Let's process non-blocking
237        if (!_isCurrentCredentialProviderInBlockingMode(request))
238        {
239            // if there was no one running, let's start with the first one
240            runningCredentialProviderIndex = Math.max(0, runningCredentialProviderIndex);
241            
242            for (; runningCredentialProviderIndex < credentialProviders.size(); runningCredentialProviderIndex++)
243            {
244                CredentialProvider runningCredentialProvider = credentialProviders.get(runningCredentialProviderIndex);
245                if (_process(request, false, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations))
246                {
247                    // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment
248                    return EMPTY_MAP;
249                }
250            }
251            
252            // No one matches
253            runningCredentialProviderIndex = -1;
254        }
255        
256        _saveLastKnownBlockingCredentialProvider(request, runningCredentialProviderIndex);
257        
258        // Let's process the current blocking one or the only existing one
259        if (_shouldRunFirstBlockingCredentialProvider(runningCredentialProviderIndex, credentialProviders, request, chosenUserPopulations))
260        {
261            CredentialProvider runningCredentialProvider = runningCredentialProviderIndex == -1 ? _getFirstBlockingCredentialProvider(credentialProviders) : credentialProviders.get(runningCredentialProviderIndex);
262            if (_process(request, true, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations))
263            {
264                // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment
265                return EMPTY_MAP;
266            }
267            
268            throw new AuthorizationRequiredException();
269        }
270        
271        // At this step we have two kind off requests
272        // 1) A secondary request of a blocking cp (such as captcha image...)
273        Integer formerRunningCredentialProviderIndex = (Integer) request.getSession(true).getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN);
274        if (formerRunningCredentialProviderIndex != null && credentialProviders.get(formerRunningCredentialProviderIndex).grantAnonymousRequest(true))
275        {
276            // Anonymous request
277            request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
278            _saveConnectingStateToSession(request, -1, true);
279            return EMPTY_MAP;
280        }
281        
282        // 2) Or a main stream request that should display the list of available blocking cp
283        return _displayBlockingList(redirector, request, credentialProviders);
284    }
285    
286    /**
287     * Prepare authentication
288     * @param redirector The redirector
289     * @param resolver The source resolver
290     * @param objectModel The object model
291     * @param source The source
292     * @param parameters The action parameters
293     * @return <code>true</code> if a user was authenticated, <code>false</code> otherwise
294     * @throws Exception if failed to prepare the authentication
295     */
296    protected boolean _preFlightCheck(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
297    {
298        Request request = ObjectModelHelper.getRequest(objectModel);
299        
300        return _handleLogout(redirector, objectModel, source, parameters)              // Test if user wants to logout
301                || _internalRequest(request)                                           // Test if this request was already authenticated or it the request is marked as an internal one
302                || _acceptedUrl(request)                                               // Test if the url is used for authentication
303                || _validateCurrentlyConnectedUser(request, redirector, parameters)    // Test if the currently connected user is still valid
304                || redirector.hasRedirected();
305    }
306    
307    /**
308     * Authenticate a user using the token in request (if configured so)
309     * @param request The request
310     * @param parameters The action parameters
311     * @return true if the user was authenticated
312     */
313    protected boolean _handleAuthenticationToken(Request request, Parameters parameters)
314    {
315        String token = request.getHeader(HEADER_TOKEN);
316        if (StringUtils.isBlank(token))
317        {
318            token = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN, _getTokenFromRequest(request));
319        }
320        
321        if (StringUtils.isNotBlank(token))
322        {
323            String context = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN_CONTEXT, "application");
324            UserIdentity userIdentity = _validateToken(token, context);
325            if (userIdentity != null)
326            {
327                // Save user identity
328                _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true);
329                _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userIdentity, request, parameters);
330                
331                if (_userStatusManager != null)
332                {
333                    _userStatusManager.updateConnectionDate(userIdentity);
334                }
335                
336                Map<String, Object> tokenArgs = Map.of("user", userIdentity);
337                ForensicLogger.info("authentication.token", tokenArgs, userIdentity);
338                
339                return true;
340            }
341        }
342        
343        return false;
344    }
345    
346    /**
347     * Get the token from the request
348     * @param request The request
349     * @return The token from the request or null
350     */
351    protected String _getTokenFromRequest(Request request)
352    {
353        // FIXME RUNTIME-2501 check the parameter is provided in POST, e.g. by seeking if '?token=' and '&token=' are not used in request uri...
354        return request.getParameter(REQUEST_PARAMETER_TOKEN);
355    }
356
357    /**
358     * Validate the given token
359     * @param token The non empty token to validate
360     * @param context the context on which the token should be validated
361     * @return The corresponding user identity or null
362     */
363    protected UserIdentity _validateToken(String token, String context)
364    {
365        return _authenticateTokenManager != null ? _authenticateTokenManager.validateToken(token, context) : null;
366    }
367    
368    private TOKEN_MODE _getTokenMode(Parameters parameters)
369    {
370        return TOKEN_MODE.valueOf(parameters.getParameter(SITEMAP_PARAMETER_TOKEN_MODE, TOKEN_MODE.DEFAULT.toString()).toUpperCase());
371    }
372
373    private void _saveLastKnownBlockingCredentialProvider(Request request, int runningCredentialProviderIndex)
374    {
375        if (runningCredentialProviderIndex != -1)
376        {
377            request.getSession(true).setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN, runningCredentialProviderIndex);
378        }
379    }
380
381    private Map _displayBlockingList(Redirector redirector, Request request, List<CredentialProvider> credentialProviders) throws IOException, ProcessingException, AuthorizationRequiredException
382    {
383        if (credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst().isPresent())
384        {
385            _saveConnectingStateToSession(request, -1, true);
386            redirector.redirect(false, getLoginURL(request));
387            return EMPTY_MAP;
388        }
389        else
390        {
391            // No way to login
392            throw new AuthorizationRequiredException();
393        }
394    }
395    
396    @SuppressWarnings("unchecked")
397    private boolean _shouldRunFirstBlockingCredentialProvider(int runningCredentialProviderIndex, List<CredentialProvider> credentialProviders, Request request, List<UserPopulation> chosenUserPopulations)
398    {
399        return runningCredentialProviderIndex >= 0 // There is a running credential provider
400            || credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).count() == 1 // There is a single blocking credential provider AND
401                && (
402                        ((List<UserPopulation>) request.getAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST)).size() == chosenUserPopulations.size() // no population choice screen
403                        || _getFirstBlockingCredentialProvider(credentialProviders).requiresNewWindow() // it does not requires a window opening
404                );
405    }
406    
407    private BlockingCredentialProvider _getFirstBlockingCredentialProvider(List<CredentialProvider> credentialProviders)
408    {
409        Optional<CredentialProvider> findFirst = credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst();
410        if (findFirst.isPresent())
411        {
412            return (BlockingCredentialProvider) findFirst.get();
413        }
414        else
415        {
416            return null;
417        }
418    }
419    
420    /**
421     * Fill the list of available users populations and credential providers
422     * @param request The request
423     * @param parameters The action parameters
424     * @param redirector The cocoon redirector
425     * @param chosenUserPopulations An empty non-null list to fill with with chosen populations
426     * @param credentialProviders An empty non-null list to fill with chosen credential providers
427     * @return true, if the population was determined, false if a redirection was required to choose
428     * @throws IOException If an error occurred
429     * @throws ProcessingException If an error occurred
430     */
431    protected boolean _prepareUserPopulationsAndCredentialProviders(Request request, Parameters parameters, Redirector redirector, List<UserPopulation> chosenUserPopulations, List<CredentialProvider> credentialProviders) throws ProcessingException, IOException
432    {
433        // Get contexts
434        List<String> contexts = _getContexts(request, parameters);
435        request.setAttribute(REQUEST_ATTRIBUTE_CONTEXTS, contexts);
436        
437        // All user populations for this context
438        List<UserPopulation> availableUserPopulations = _getAvailableUserPopulationsIds(request, contexts).stream().map(_userPopulationDAO::getUserPopulation).collect(Collectors.toList());
439        request.setAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST, availableUserPopulations);
440        
441        // Chosen population
442        String userPopulationId = _getChosenUserPopulationId(request, availableUserPopulations);
443        request.setAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID, userPopulationId);
444        
445        chosenUserPopulations.addAll(userPopulationId == null ? availableUserPopulations : Collections.singletonList(_userPopulationDAO.getUserPopulation(userPopulationId)));
446        if (chosenUserPopulations.size() == 0)
447        {
448            String redirection = parameters.getParameter("nocontext-redirection", null);
449            if (redirection == null)
450            {
451                throw new IllegalStateException("There is no populations available for contexts '" + StringUtils.join(contexts, "', '") + "'");
452            }
453            else
454            {
455                redirector.redirect(false, redirection);
456                return false;
457            }
458        }
459
460        // Get possible credential providers
461        boolean availableCredentialProviders = _hasCredentialProviders(chosenUserPopulations);
462        request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST, availableCredentialProviders);
463
464        // null means the credential providers cannot be determine without knowing population first
465        if (!availableCredentialProviders)
466        {
467            request.setAttribute(REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST, true);
468            
469            // if we are on this screen after a 'back' button hit, we need to reset connecting information
470            _resetConnectingStateToSession(request);
471            
472            // Screen "Where Are You From?" with the list of populations to select
473            if (redirector != null)
474            {
475                redirector.redirect(false, getLoginURL(request));
476            }
477            return false;
478        }
479        else
480        {
481            credentialProviders.addAll(chosenUserPopulations.get(0).getCredentialProviders());
482            if (credentialProviders.size() == 0)
483            {
484                throw new IllegalStateException("There is no populations credential provider available for contexts '" + StringUtils.join(contexts, "', '") + "'");
485            }
486            request.setAttribute(REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST, userPopulationId == null || _hasCredentialProviders(availableUserPopulations) || credentialProviders.size() == 1 && !credentialProviders.stream().filter(cp -> cp instanceof FormCredentialProvider).findAny().isPresent());
487            return true;
488        }
489    }
490
491    /**
492     * Get the url for the redirector to display the login screen
493     * @param request The request
494     * @return The url. Cannot be null or empty
495     */
496    protected String getLoginURL(Request request)
497    {
498        return getLoginURLParameters(request, "cocoon://_plugins/core/login.html");
499    }
500    
501    
502    /**
503     * Get the url for the redirector to display the login screen
504     * @param request The request
505     * @param baseURL The url to complete with parameters
506     * @return The url. Cannot be null or empty
507     */
508    @SuppressWarnings("unchecked")
509    protected String getLoginURLParameters(Request request, String baseURL)
510    {
511        List<String> parameters = new ArrayList<>();
512        
513        Boolean invalidPopulationIds = (Boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INVALID_POPULATION);
514        parameters.add("invalidPopulationIds=" + (invalidPopulationIds == Boolean.TRUE ? "true" : "false"));
515        
516        boolean shouldDisplayUserPopulationsList = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST);
517        parameters.add("shouldDisplayUserPopulationsList=" + (shouldDisplayUserPopulationsList ? "true" : "false"));
518        
519        List<UserPopulation> usersPopulations = (List<UserPopulation>) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST);
520        if (usersPopulations != null)
521        {
522            parameters.add("usersPopulations=" + URIUtils.encodeParameter(usersPopulations.stream().map(UserPopulation::getId).collect(Collectors.joining(","))));
523        }
524        
525        String chosenPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID);
526        if (chosenPopulationId != null)
527        {
528            parameters.add("chosenPopulationId=" + URIUtils.encodeParameter(chosenPopulationId));
529        }
530        
531        boolean availableCredentialProviders = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST);
532        parameters.add("availableCredentialProviders=" + (availableCredentialProviders ? "true" : "false"));
533        
534        Integer credentialProviderIndex = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX);
535        parameters.add("credentialProviderIndex=" + String.valueOf(credentialProviderIndex != null ? credentialProviderIndex : -1));
536        
537        List<String> contexts = (List<String>) request.getAttribute(REQUEST_ATTRIBUTE_CONTEXTS);
538        parameters.add("contexts=" + URIUtils.encodeParameter(StringUtils.join(contexts, ",")));
539        
540        return baseURL + (baseURL.contains("?") ? "&" : "?") + StringUtils.join(parameters, "&");
541    }
542    
543    /**
544     * Get the url for the redirector to display the logout screen
545     * @param request The request
546     * @return The url. Cannot be null or empty
547     */
548    protected String getLogoutURL(Request request)
549    {
550        return "cocoon://_plugins/core/logout.html";
551    }
552    
553    /**
554     * Determine if there is a list of credential providers to use
555     * @param userPopulations The list of applicable user populations
556     * @return true if credentialproviders can be used
557     */
558    protected boolean _hasCredentialProviders(List<UserPopulation> userPopulations)
559    {
560        // Is there only one population or all populations have the same credential provider list as the first one?
561        if (userPopulations.size() == 1
562            || userPopulations.stream().map(UserPopulation::getCredentialProviders).distinct().count() == 1
563                && userPopulations.stream().map(this::_needsResetLinkOnFormCredential).distinct().count() == 1)
564        {
565            return true;
566        }
567
568        // Cannot determine the list
569        return false;
570    }
571    
572    /**
573     * Check if the user population include a {@link FormCredentialProvider}
574     * that requires display of a reset password link
575     * @param userPopulation the user population
576     * @return true if a reset password link needs to be displayed
577     */
578    private boolean _needsResetLinkOnFormCredential(UserPopulation userPopulation)
579    {
580        if (userPopulation.getCredentialProviders().stream()
581            .filter(FormCredentialProvider.class::isInstance)
582            .map(FormCredentialProvider.class::cast)
583            .map(FormCredentialProvider::displayResetLink)
584            .findAny().orElse(false))
585        {
586            return userPopulation.getUserDirectories().stream()
587                    .anyMatch(ModifiableUserDirectory.class::isInstance);
588        }
589        return false;
590    }
591    
592    /**
593     * Get the available populations for the given contexts
594     * @param request The request
595     * @param contexts The contexts
596     * @return The non-null list of populations
597     */
598    protected Set<String> _getAvailableUserPopulationsIds(Request request, List<String> contexts)
599    {
600        return _populationContextHelper.getUserPopulationsOnContexts(contexts, false, false);
601    }
602
603    /**
604     * Get the population for the given context
605     * @param request The request
606     * @param availableUserPopulations The available users populations
607     * @return The chosen population id. Can be null.
608     */
609    protected String _getChosenUserPopulationId(Request request, List<UserPopulation> availableUserPopulations)
610    {
611        // Get request population choice
612        String userPopulationId = request.getParameter(REQUEST_PARAMETER_POPULATION_NAME);
613        if (userPopulationId == null)
614        {
615            // Get memorized population choice
616            Session session = request.getSession(false);
617            if (session != null)
618            {
619                userPopulationId = (String) session.getAttribute(SESSION_CONNECTING_USERPOPULATION_ID);
620            }
621        }
622        
623        // A population choice was already made
624        if (StringUtils.isNotBlank(userPopulationId))
625        {
626            final String finalUserPopulationId = userPopulationId;
627            if (availableUserPopulations.stream().anyMatch(userPopulation -> userPopulation.getId().equals(finalUserPopulationId)))
628            {
629                return userPopulationId;
630            }
631            else
632            {
633                // Wrong submitted population id
634                request.setAttribute(REQUEST_ATTRIBUTE_INVALID_POPULATION, true);
635            }
636        }
637        
638        return null;
639    }
640    
641    /**
642     * Try to authenticate with this credential provider in this mode. Delegates to _doProcess
643     * @param request The request
644     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
645     * @param runningCredentialProvider the Credential provider to test
646     * @param runningCredentialProviderIndex The index of the currently tested credential provider
647     * @param redirector The cocoon redirector
648     * @param userPopulations The list of possible user populations
649     * @return false if we should try with another Credential provider, true otherwise
650     * @throws Exception If an error occurred
651     */
652    protected boolean _process(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, int runningCredentialProviderIndex, Redirector redirector, List<UserPopulation> userPopulations) throws Exception
653    {
654        boolean existingSession = request.getSession(false) != null;
655        _saveConnectingStateToSession(request, runningBlockingkMode ? -1 : runningCredentialProviderIndex, runningBlockingkMode);
656        if (_doProcess(request, runningBlockingkMode, runningCredentialProvider, redirector, userPopulations))
657        {
658            return true;
659        }
660        if (existingSession)
661        {
662            // A session was created but finally we do not need it
663            request.getSession().invalidate();
664        }
665        return false;
666    }
667    
668    /**
669     * Try to authenticate with this credential provider in this mode
670     * @param request The request
671     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
672     * @param runningCredentialProvider the Credential provider to test
673     * @param redirector The cocoon redirector
674     * @param userPopulations The list of possible user populations
675     * @return false if we should try with another Credential provider, true otherwise
676     * @throws Exception If an error occurred
677     */
678
679    protected boolean _doProcess(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, Redirector redirector, List<UserPopulation> userPopulations) throws Exception
680    {
681        if (runningCredentialProvider.grantAnonymousRequest(runningBlockingkMode))
682        {
683            // Anonymous request
684            request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
685            return true;
686        }
687        
688        UserIdentity potentialUserIdentity = null;
689        try
690        {
691            
692            potentialUserIdentity = runningCredentialProvider.getUserIdentity(runningBlockingkMode, redirector);
693        }
694        catch (WeakPasswordException e)
695        {
696            // User credentials are ok but user use a weak password
697            potentialUserIdentity = e.getUserIdentity();
698            _handleWeakPassword(request, runningCredentialProvider, redirector, potentialUserIdentity);
699        }
700        
701        if (redirector.hasRedirected())
702        {
703            // getCredentials require a redirection, save state and proceed
704            return true;
705        }
706        else if (potentialUserIdentity == null)
707        {
708            // Let us try another credential provider
709            return false;
710        }
711        
712        // Check if user exists
713        UserIdentity userIdentity = _getUserIdentity(userPopulations, potentialUserIdentity, redirector, runningBlockingkMode, runningCredentialProvider);
714        if (redirector.hasRedirected())
715        {
716            // getCredentials require a redirection, save state and proceed
717            return true;
718        }
719        else if (userIdentity == null)
720        {
721            // Let us try another credential provider
722            return false;
723        }
724
725        // Save user identity
726        _setUserIdentityInSession(request, userIdentity, runningCredentialProvider, runningBlockingkMode);
727        
728        // Authentication succeeded
729        runningCredentialProvider.userAllowed(runningBlockingkMode, userIdentity, redirector);
730        
731        if (_userStatusManager != null)
732        {
733            _userStatusManager.updateConnectionDate(userIdentity);
734        }
735        
736        _logLoginEvent(runningCredentialProvider, userIdentity);
737        
738        return true;
739    }
740    
741    /**
742     * Handle weak password exception
743     * @param request the request
744     * @param runningCredentialProvider the credential provider that detected the weak password
745     * @param redirector the redirector
746     * @param userIdentity the user identity with a weak password
747     * @throws Exception if an error occurred
748     */
749    protected void _handleWeakPassword(Request request, CredentialProvider runningCredentialProvider, Redirector redirector, UserIdentity userIdentity) throws Exception
750    {
751        ForensicLogger.info("authentication.form.weak.password", Map.of("userIdentity", userIdentity), userIdentity);
752        Optional<String> resetPasswordURI = _getWeakPasswordURI(request, userIdentity);
753        if (resetPasswordURI.isPresent())
754        {
755            // Force redirect to weak password url to force change password
756            getLogger().info("Password of user " + userIdentity + " does not meet the security requirements. Force to change password.");
757            redirector.redirect(false, resetPasswordURI.get());
758            return;
759        }
760        // Defaut implementation only log that user has a weak password. User will be authenticated
761        getLogger().warn("Password of user " + userIdentity + " does not meet the security requirements. User is authenticated despite the risk for security.");
762    }
763    
764    /**
765     * Get the URI where the user should be redirected after a weak password is detected
766     * @param request the current request
767     * @param userIdentity the user identity with a weak password
768     * @return the absolute uri
769     */
770    protected Optional<String> _getWeakPasswordURI(Request request, UserIdentity userIdentity)
771    {
772        // do not force password change in safe or maintenance mode
773        if (_userPasswordManager != null && RuntimeServlet.getRunMode() == RunMode.NORMAL)
774        {
775            return _userPasswordManager.getChangePasswordURI(request, userIdentity, true);
776        }
777        return Optional.empty();
778    }
779
780    /**
781     * Log login event
782     * @param credentialProvider the running credential provider
783     * @param userIdentity the user identity
784     */
785    protected void _logLoginEvent(CredentialProvider credentialProvider, UserIdentity userIdentity)
786    {
787        Map<String, Object> loginArgs = Map.of("credential-provider", credentialProvider.getCredentialProviderModelId(), "user", userIdentity);
788        ForensicLogger.info("authentication.login", loginArgs, userIdentity);
789    }
790    
791    /**
792     * Log logout event
793     * @param userIdentity the user identity
794     */
795    protected void _logLogoutEvent(UserIdentity userIdentity)
796    {
797        Map<String, Object> logoutArgs = Map.of("user", userIdentity);
798        ForensicLogger.info("authentication.logout", logoutArgs, userIdentity);
799    }
800    
801    /**
802     * Reset the connecting information in session
803     * @param request The request
804     */
805    protected static void _resetConnectingStateToSession(Request request)
806    {
807        Session session = request.getSession(false);
808        if (session != null)
809        {
810            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
811            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
812            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN);
813            session.removeAttribute(SESSION_CONNECTING_USERPOPULATION_ID);
814        }
815    }
816    
817    /**
818     * When the process end successfully, save the state
819     * @param request The request
820     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
821     * @param runningCredentialProviderIndex the currently tested credential provider
822     */
823    protected void _saveConnectingStateToSession(Request request, int runningCredentialProviderIndex, boolean runningBlockingkMode)
824    {
825        Session session = request.getSession(true);
826        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, runningCredentialProviderIndex);
827        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE, runningBlockingkMode);
828        session.setAttribute(SESSION_CONNECTING_USERPOPULATION_ID, request.getAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID));
829    }
830
831    /**
832     * Save user identity in request
833     * @param request The request
834     * @param userIdentity The useridentity to save
835     * @param credentialProvider The credential provider used to connect
836     * @param blockingMode The mode used for the credential provider
837     */
838    protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
839    {
840        setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode);
841        if (_observationManager != null)
842        {
843            Map<String, Object> eventParams = new HashMap<>();
844            eventParams.put(ObservationConstants.ARGS_USER, userIdentity);
845            _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams));
846        }
847    }
848    
849    /**
850     * Save user identity in request
851     * @param request The request
852     * @param userIdentity The useridentity to save
853     * @param credentialProvider The credential provider used to connect
854     * @param blockingMode The mode used for the credential provider
855     */
856    public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
857    {
858        Session session = renewSession(request);
859        _resetConnectingStateToSession(request);
860        session.setAttribute(SESSION_USERIDENTITY, userIdentity);
861        session.setAttribute(SESSION_CREDENTIALPROVIDER, credentialProvider);
862        session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE, blockingMode);
863    }
864    
865    /**
866     * Change the session id (for security purposes)
867     * @param request The current request
868     * @return The new session
869     */
870    public static Session renewSession(Request request)
871    {
872        Session session = request.getSession(true);
873        
874        Map<String, Object> attributesCopy = new HashMap<>();
875        
876        Enumeration<String> attributeNames = session.getAttributeNames();
877        while (attributeNames.hasMoreElements())
878        {
879            String attributeName = attributeNames.nextElement();
880            attributesCopy.put(attributeName, session.getAttribute(attributeName));
881        }
882        
883        session.invalidate();
884        
885        session = request.getSession(true);
886        
887        for (Entry<String, Object> attribute : attributesCopy.entrySet())
888        {
889            session.setAttribute(attribute.getKey(), attribute.getValue());
890        }
891        
892        return session;
893    }
894
895    /**
896     * Get the user identity of the connected user from the session
897     * @param request The request
898     * @return The connected useridentity or null
899     */
900    protected UserIdentity _getUserIdentityFromSession(Request request)
901    {
902        return getUserIdentityFromSession(request);
903    }
904    
905    /**
906     * Get the user identity of the connected user from the session
907     * @param request The request
908     * @return The connected useridentity or null
909     */
910    public static UserIdentity getUserIdentityFromSession(Request request)
911    {
912        Session session = request.getSession(false);
913        if (session != null)
914        {
915            return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY);
916        }
917        return null;
918    }
919   
920    /**
921     * Get the credential provider used for the current connection
922     * @param request The request
923     * @return The credential provider used or null
924     */
925    protected CredentialProvider _getCredentialProviderFromSession(Request request)
926    {
927        return getCredentialProviderFromSession(request);
928    }
929    
930    /**
931     * Get the credential provider used for the current connection
932     * @param request The request
933     * @return The credential provider used or null
934     */
935    public static CredentialProvider getCredentialProviderFromSession(Request request)
936    {
937        Session session = request.getSession(false);
938        if (session != null)
939        {
940            return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER);
941        }
942        return null;
943    }
944    
945    /**
946     * Get the credential provider mode used for the current connection
947     * @param request The request
948     * @return The credential provider mode used or null
949     */
950    protected Boolean _getCredentialProviderModeFromSession(Request request)
951    {
952        return getCredentialProviderModeFromSession(request);
953    }
954    
955    /**
956     * Get the credential provider mode used for the current connection
957     * @param request The request
958     * @return The credential provider mode used or null
959     */
960    public static Boolean getCredentialProviderModeFromSession(Request request)
961    {
962        Session session = request.getSession(false);
963        if (session != null)
964        {
965            return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE);
966        }
967        return null;
968    }
969
970    /**
971     * If there is a running credential provider, was it in non-blocking or blocking mode?
972     * @param request The request
973     * @return false if non-blocking, true if blocking
974     */
975    protected boolean _isCurrentCredentialProviderInBlockingMode(Request request)
976    {
977        if (Strings.CS.equals(request.getParameter(REQUEST_PARAMETER_NONBLOCING), "force"))
978        {
979            return false;
980        }
981        
982        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
983        if (requestedCredentialParameterIndex != null && requestedCredentialParameterIndex != -1)
984        {
985            return true;
986        }
987        
988        Session session = request.getSession(false);
989        if (session != null)
990        {
991            Boolean mode = (Boolean) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
992            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
993            if (mode != null)
994            {
995                return mode.booleanValue();
996            }
997        }
998        return false;
999    }
1000    
1001    /**
1002     * Call this to skip the currently used credential provider and proceed to the next one.
1003     * Useful for non blocking
1004     * @param request The request
1005     */
1006    public static void skipCurrentCredentialProvider(Request request)
1007    {
1008        Session session = request.getSession();
1009        if (session != null)
1010        {
1011            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1012            if (cpIndex != null)
1013            {
1014                cpIndex++;
1015                session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, cpIndex);
1016            }
1017        }
1018    }
1019
1020    /**
1021     * Get the current credential provider index or -1 if there no running provider FROM REQUEST PARAMETER
1022     * @param request The request
1023     * @return The credential provider index to use in the availablesCredentialProviders list or -1 or null
1024     */
1025    protected Integer _getCurrentCredentialProviderIndexFromParameter(Request request)
1026    {
1027        // Is the CP requested?
1028        String requestedCredentialParameterIndex = request.getParameter(REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX);
1029        if (StringUtils.isNotBlank(requestedCredentialParameterIndex))
1030        {
1031            int index = Integer.parseInt(requestedCredentialParameterIndex);
1032            return index;
1033        }
1034        return null;
1035    }
1036    
1037    /**
1038     * Get the current credential provider index or -1 if there no running provider
1039     * @param request The request
1040     * @param availableCredentialProviders The list of available credential provider
1041     * @return The credential provider index to use in the availablesCredentialProviders list or -1
1042     */
1043    protected int _getCurrentCredentialProviderIndex(Request request, List<CredentialProvider> availableCredentialProviders)
1044    {
1045        // Is the CP requested?
1046        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
1047        if (requestedCredentialParameterIndex != null)
1048        {
1049            if (requestedCredentialParameterIndex < availableCredentialProviders.size())
1050            {
1051                return requestedCredentialParameterIndex;
1052            }
1053            else
1054            {
1055                return -1;
1056            }
1057        }
1058        
1059        // Was the CP memorized?
1060        Session session = request.getSession(false);
1061        if (session != null)
1062        {
1063            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1064            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1065            
1066            if (cpIndex != null)
1067            {
1068                return cpIndex;
1069            }
1070        }
1071        
1072        // Default value
1073        return -1;
1074    }
1075    
1076    /**
1077     * Get the authentication context
1078     * @param request The request
1079     * @param parameters The action parameters
1080     * @return The context
1081     * @throws IllegalArgumentException If there is no context set
1082     */
1083    protected List<String> _getContexts(Request request, Parameters parameters)
1084    {
1085        String context = parameters.getParameter("context", null);
1086        if (context == null)
1087        {
1088            throw new IllegalArgumentException("The authentication is not parameterized correctly: an authentication context must be specified");
1089        }
1090        return Collections.singletonList(context);
1091    }
1092
1093    /**
1094     * Determine if the request is internal and do not need authentication
1095     * @param request The request
1096     * @return true to bypass this authentication
1097     */
1098    protected boolean _internalRequest(Request request)
1099    {
1100        return "true".equals(request.getAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED)) || request.getAttribute(REQUEST_ATTRIBUTE_INTERNAL_ALLOWED) != null;
1101    }
1102    
1103    /**
1104     * Determine if the request is one of the authentication process (except the credential providers)
1105     * @param request The request
1106     * @return true to bypass this authentication
1107     */
1108    protected boolean _acceptedUrl(Request request)
1109    {
1110        // URL without server context and leading slash.
1111        String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL);
1112        for (Pattern pattern : _acceptedUrlPatterns)
1113        {
1114            if (pattern.matcher(url).matches())
1115            {
1116                // Anonymous request
1117                request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
1118
1119                return true;
1120            }
1121        }
1122        
1123        return false;
1124    }
1125
1126    /**
1127     * This method ensure that there is a currently connected user and that it is still valid
1128     * @param request The request
1129     * @param redirector The cocoon redirector
1130     * @param parameters The action parameters
1131     * @return true if the user is connected and valid
1132     * @throws Exception if an error occurred
1133     */
1134    protected boolean _validateCurrentlyConnectedUser(Request request, Redirector redirector, Parameters parameters) throws Exception
1135    {
1136        Session session = request.getSession(false);
1137        UserIdentity userCurrentlyConnected = _getUserIdentityFromSession(request);
1138        CredentialProvider runningCredentialProvider = _getCredentialProviderFromSession(request);
1139        Boolean runningBlockingkMode = _getCredentialProviderModeFromSession(request);
1140        
1141        if (runningCredentialProvider == null || userCurrentlyConnected == null || runningBlockingkMode == null || !runningCredentialProvider.isStillConnected(runningBlockingkMode, userCurrentlyConnected, redirector))
1142        {
1143            if (redirector.hasRedirected())
1144            {
1145                return true;
1146            }
1147            
1148            // There is an invalid connected user
1149            if (session != null && userCurrentlyConnected != null)
1150            {
1151                session.invalidate();
1152            }
1153            return false;
1154        }
1155        
1156        // let us make an exception for the user image url since we need it on the 403 page
1157        if (RuntimeServlet.getRunMode() == RunMode.MAINTENANCE && MaintenanceAction.acceptedUrl(request))
1158        {
1159            return true;
1160        }
1161        
1162        _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userCurrentlyConnected, request, parameters);
1163        
1164        return true;
1165    }
1166    
1167    /**
1168     * This method is the second part of the process that ensure that there is a currently connected user and that it is still valid
1169     * @param userCurrentlyConnected The user to test
1170     * @param request The request
1171     * @param parameters The action parameters
1172     */
1173    protected void _validateCurrentlyConnectedUserIsInAuthorizedPopulation(UserIdentity userCurrentlyConnected, Request request, Parameters parameters)
1174    {
1175        if (_getTokenMode(parameters) == TOKEN_MODE.DEFAULT)
1176        {
1177            // we know this is a valid user, but we need to check if the context is correct
1178            List<String> contexts = _getContexts(request, parameters);
1179            // All user populations for this context
1180            Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, contexts);
1181            
1182            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1183            {
1184                throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated to the contexts '" + StringUtils.join(contexts, "', '") + "' because its populations are not part of the " + availableUserPopulationsIds.size() + " granted populations.");
1185            }
1186        }
1187        else
1188        {
1189            // In 'token only' mode, check if user is part of the active populations (regardless of the context)
1190            List<String> availableUserPopulationsIds = _userPopulationDAO.getEnabledUserPopulations(false).stream().map(UserPopulation::getId).collect(Collectors.toList());
1191            
1192            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1193            {
1194                throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated because its populations does not exist or it is disabled.");
1195            }
1196        }
1197    }
1198    
1199    /**
1200     * Test if user wants to logout and handle it
1201     * @param redirector The cocoon redirector
1202     * @param objectModel The cocoon object model
1203     * @param source The sitemap source
1204     * @param parameters The sitemap parameters
1205     * @return true if the user was logged out
1206     * @throws Exception if an error occurred
1207     */
1208    protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
1209    {
1210        Request request = ObjectModelHelper.getRequest(objectModel);
1211        if (Strings.CS.equals(request.getContextPath() + request.getAttribute(WorkspaceMatcher.WORKSPACE_URI) + "/logout.html", request.getRequestURI())
1212                || Strings.CS.equals("true", parameters.getParameter("logout", "false")))
1213        {
1214            // The user logs out
1215            UserIdentity currentUser = _currentUserProvider.getUser();
1216            if (currentUser != null) // user can be null when calling the url with an expired session
1217            {
1218                _currentUserProvider.logout(redirector);
1219                _logLogoutEvent(currentUser);
1220            }
1221            
1222            if (!redirector.hasRedirected())
1223            {
1224                redirector.redirect(false, getLogoutURL(request));
1225            }
1226            
1227            return true;
1228        }
1229        return false;
1230    }
1231    
1232    /**
1233     * Check the authentications of the authentication manager
1234     * @param userPopulations The list of available matching populations
1235     * @param redirector The cocoon redirector
1236     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
1237     * @param runningCredentialProvider The Credential provider to test
1238     * @param potentialUserIdentity A possible user identity. Population can be null. User may not exist either.
1239     * @return The user population matching credentials or null
1240     * @throws Exception If an error occurred
1241     * @throws AccessDeniedException If the user is rejected
1242     */
1243    protected UserIdentity _getUserIdentity(List<UserPopulation> userPopulations, UserIdentity potentialUserIdentity, Redirector redirector, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider) throws Exception
1244    {
1245        if (potentialUserIdentity.getPopulationId() == null)
1246        {
1247            for (UserPopulation up : userPopulations)
1248            {
1249                User user = _userManager.getUser(up, potentialUserIdentity.getLogin());
1250                if (_isLoginCaseExact(user, potentialUserIdentity))
1251                {
1252                    return user.getIdentity();
1253                }
1254            }
1255        }
1256        else
1257        {
1258            User user = _userManager.getUser(potentialUserIdentity.getPopulationId(), potentialUserIdentity.getLogin());
1259            if (_isLoginCaseExact(user, potentialUserIdentity))
1260            {
1261                return user.getIdentity();
1262            }
1263        }
1264        
1265        runningCredentialProvider.userNotAllowed(runningBlockingkMode, redirector);
1266        
1267        if (getLogger().isWarnEnabled())
1268        {
1269            getLogger().warn("The user '" + potentialUserIdentity + "' was authenticated by the credential provider '" + runningCredentialProvider.getCredentialProviderModelId() + "' but it does not match any user of the " + userPopulations.size() + " granted populations.");
1270        }
1271        
1272        return null;
1273    }
1274    
1275    private boolean _isLoginCaseExact(User user, UserIdentity potentialUserIdentity)
1276    {
1277        return user != null
1278                && (user.getUserDirectory().isCaseSensitive() && Strings.CS.equals(user.getIdentity().getLogin(), potentialUserIdentity.getLogin())
1279                    || !user.getUserDirectory().isCaseSensitive() && Strings.CI.equals(user.getIdentity().getLogin(), potentialUserIdentity.getLogin()));
1280    }
1281}