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