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;
044
045import org.ametys.core.ObservationConstants;
046import org.ametys.core.authentication.token.AuthenticationTokenManager;
047import org.ametys.core.observation.Event;
048import org.ametys.core.observation.ObservationManager;
049import org.ametys.core.trace.ForensicLogger;
050import org.ametys.core.user.CurrentUserProvider;
051import org.ametys.core.user.User;
052import org.ametys.core.user.UserIdentity;
053import org.ametys.core.user.UserManager;
054import org.ametys.core.user.directory.ModifiableUserDirectory;
055import org.ametys.core.user.directory.WeakPasswordException;
056import org.ametys.core.user.population.PopulationContextHelper;
057import org.ametys.core.user.population.UserPopulation;
058import org.ametys.core.user.population.UserPopulationDAO;
059import org.ametys.core.user.status.UserStatusManager;
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    /** The user status manager */
161    protected UserStatusManager _userStatusManager;
162
163    /**
164     * The token mode of this authentication action
165     */
166    protected enum TOKEN_MODE
167    {
168        /** In this mode, only the token will be taken in account. If no token is found, authentication will not be considered done */
169        TOKEN_ONLY,
170        /** 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 */
171        ALLOW_ANONYMOUS,
172        /** In this default mode, the token will be taken in account, but if no token is found, the authentication process will continue */
173        DEFAULT
174    }
175    
176    @Override
177    public void initialize() throws Exception
178    {
179        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
180        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
181        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
182        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
183        
184        // These component are not safe but the action is
185        if (manager.hasService(UserPasswordManager.ROLE))
186        {
187            _userPasswordManager = (UserPasswordManager) manager.lookup(UserPasswordManager.ROLE);
188            _userStatusManager = (UserStatusManager) manager.lookup(UserStatusManager.ROLE);
189            _authenticateTokenManager = (AuthenticationTokenManager) manager.lookup(AuthenticationTokenManager.ROLE);
190            _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
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                
332                if (_userStatusManager != null)
333                {
334                    _userStatusManager.updateConnectionDate(userIdentity);
335                }
336                
337                Map<String, Object> tokenArgs = Map.of("user", userIdentity);
338                ForensicLogger.info("authentication.token", tokenArgs, userIdentity);
339                
340                return true;
341            }
342        }
343        
344        return false;
345    }
346    
347    /**
348     * Get the token from the request
349     * @param request The request
350     * @return The token from the request or null
351     */
352    protected String _getTokenFromRequest(Request request)
353    {
354        // FIXME RUNTIME-2501 check the parameter is provided in POST, e.g. by seeking if '?token=' and '&token=' are not used in request uri...
355        return request.getParameter(REQUEST_PARAMETER_TOKEN);
356    }
357
358    /**
359     * Validate the given token
360     * @param token The non empty token to validate
361     * @param context the context on which the token should be validated
362     * @return The corresponding user identity or null
363     */
364    protected UserIdentity _validateToken(String token, String context)
365    {
366        return _authenticateTokenManager != null ? _authenticateTokenManager.validateToken(token, context) : null;
367    }
368    
369    private TOKEN_MODE _getTokenMode(Parameters parameters)
370    {
371        return TOKEN_MODE.valueOf(parameters.getParameter(SITEMAP_PARAMETER_TOKEN_MODE, TOKEN_MODE.DEFAULT.toString()).toUpperCase());
372    }
373
374    private void _saveLastKnownBlockingCredentialProvider(Request request, int runningCredentialProviderIndex)
375    {
376        if (runningCredentialProviderIndex != -1)
377        {
378            request.getSession(true).setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN, runningCredentialProviderIndex);
379        }
380    }
381
382    private Map _displayBlockingList(Redirector redirector, Request request, List<CredentialProvider> credentialProviders) throws IOException, ProcessingException, AuthorizationRequiredException
383    {
384        if (credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst().isPresent())
385        {
386            _saveConnectingStateToSession(request, -1, true);
387            redirector.redirect(false, getLoginURL(request));
388            return EMPTY_MAP;
389        }
390        else
391        {
392            // No way to login
393            throw new AuthorizationRequiredException();
394        }
395    }
396    
397    @SuppressWarnings("unchecked")
398    private boolean _shouldRunFirstBlockingCredentialProvider(int runningCredentialProviderIndex, List<CredentialProvider> credentialProviders, Request request, List<UserPopulation> chosenUserPopulations)
399    {
400        return runningCredentialProviderIndex >= 0 // There is a running credential provider
401            || credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).count() == 1 // There is a single blocking credential provider AND
402                && (
403                        ((List<UserPopulation>) request.getAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST)).size() == chosenUserPopulations.size() // no population choice screen
404                        || _getFirstBlockingCredentialProvider(credentialProviders).requiresNewWindow() // it does not requires a window opening
405                );
406    }
407    
408    private BlockingCredentialProvider _getFirstBlockingCredentialProvider(List<CredentialProvider> credentialProviders)
409    {
410        Optional<CredentialProvider> findFirst = credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst();
411        if (findFirst.isPresent())
412        {
413            return (BlockingCredentialProvider) findFirst.get();
414        }
415        else
416        {
417            return null;
418        }
419    }
420    
421    /**
422     * Fill the list of available users populations and credential providers
423     * @param request The request
424     * @param parameters The action parameters
425     * @param redirector The cocoon redirector
426     * @param chosenUserPopulations An empty non-null list to fill with with chosen populations
427     * @param credentialProviders An empty non-null list to fill with chosen credential providers
428     * @return true, if the population was determined, false if a redirection was required to choose
429     * @throws IOException If an error occurred
430     * @throws ProcessingException If an error occurred
431     */
432    protected boolean _prepareUserPopulationsAndCredentialProviders(Request request, Parameters parameters, Redirector redirector, List<UserPopulation> chosenUserPopulations, List<CredentialProvider> credentialProviders) throws ProcessingException, IOException
433    {
434        // Get contexts
435        List<String> contexts = _getContexts(request, parameters);
436        request.setAttribute(REQUEST_ATTRIBUTE_CONTEXTS, contexts);
437        
438        // All user populations for this context
439        List<UserPopulation> availableUserPopulations = _getAvailableUserPopulationsIds(request, contexts).stream().map(_userPopulationDAO::getUserPopulation).collect(Collectors.toList());
440        request.setAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST, availableUserPopulations);
441        
442        // Chosen population
443        String userPopulationId = _getChosenUserPopulationId(request, availableUserPopulations);
444        request.setAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID, userPopulationId);
445        
446        chosenUserPopulations.addAll(userPopulationId == null ? availableUserPopulations : Collections.singletonList(_userPopulationDAO.getUserPopulation(userPopulationId)));
447        if (chosenUserPopulations.size() == 0)
448        {
449            String redirection = parameters.getParameter("nocontext-redirection", null);
450            if (redirection == null)
451            {
452                throw new IllegalStateException("There is no populations available for contexts '" + StringUtils.join(contexts, "', '") + "'");
453            }
454            else
455            {
456                redirector.redirect(false, redirection);
457                return false;
458            }
459        }
460
461        // Get possible credential providers
462        boolean availableCredentialProviders = _hasCredentialProviders(chosenUserPopulations);
463        request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST, availableCredentialProviders);
464
465        // null means the credential providers cannot be determine without knowing population first
466        if (!availableCredentialProviders)
467        {
468            request.setAttribute(REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST, true);
469            
470            // if we are on this screen after a 'back' button hit, we need to reset connecting information
471            _resetConnectingStateToSession(request);
472            
473            // Screen "Where Are You From?" with the list of populations to select
474            if (redirector != null)
475            {
476                redirector.redirect(false, getLoginURL(request));
477            }
478            return false;
479        }
480        else
481        {
482            credentialProviders.addAll(chosenUserPopulations.get(0).getCredentialProviders());
483            if (credentialProviders.size() == 0)
484            {
485                throw new IllegalStateException("There is no populations credential provider available for contexts '" + StringUtils.join(contexts, "', '") + "'");
486            }
487            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());
488            return true;
489        }
490    }
491
492    /**
493     * Get the url for the redirector to display the login screen
494     * @param request The request
495     * @return The url. Cannot be null or empty
496     */
497    protected String getLoginURL(Request request)
498    {
499        return getLoginURLParameters(request, "cocoon://_plugins/core/login.html");
500    }
501    
502    
503    /**
504     * Get the url for the redirector to display the login screen
505     * @param request The request
506     * @param baseURL The url to complete with parameters
507     * @return The url. Cannot be null or empty
508     */
509    @SuppressWarnings("unchecked")
510    protected String getLoginURLParameters(Request request, String baseURL)
511    {
512        List<String> parameters = new ArrayList<>();
513        
514        Boolean invalidPopulationIds = (Boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INVALID_POPULATION);
515        parameters.add("invalidPopulationIds=" + (invalidPopulationIds == Boolean.TRUE ? "true" : "false"));
516        
517        boolean shouldDisplayUserPopulationsList = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST);
518        parameters.add("shouldDisplayUserPopulationsList=" + (shouldDisplayUserPopulationsList ? "true" : "false"));
519        
520        List<UserPopulation> usersPopulations = (List<UserPopulation>) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST);
521        if (usersPopulations != null)
522        {
523            parameters.add("usersPopulations=" + URIUtils.encodeParameter(usersPopulations.stream().map(UserPopulation::getId).collect(Collectors.joining(","))));
524        }
525        
526        String chosenPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID);
527        if (chosenPopulationId != null)
528        {
529            parameters.add("chosenPopulationId=" + URIUtils.encodeParameter(chosenPopulationId));
530        }
531        
532        boolean availableCredentialProviders = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST);
533        parameters.add("availableCredentialProviders=" + (availableCredentialProviders ? "true" : "false"));
534        
535        Integer credentialProviderIndex = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX);
536        parameters.add("credentialProviderIndex=" + String.valueOf(credentialProviderIndex != null ? credentialProviderIndex : -1));
537        
538        List<String> contexts = (List<String>) request.getAttribute(REQUEST_ATTRIBUTE_CONTEXTS);
539        parameters.add("contexts=" + URIUtils.encodeParameter(StringUtils.join(contexts, ",")));
540        
541        return baseURL + (baseURL.contains("?") ? "&" : "?") + StringUtils.join(parameters, "&");
542    }
543    
544    /**
545     * Get the url for the redirector to display the logout screen
546     * @param request The request
547     * @return The url. Cannot be null or empty
548     */
549    protected String getLogoutURL(Request request)
550    {
551        return "cocoon://_plugins/core/logout.html";
552    }
553    
554    /**
555     * Determine if there is a list of credential providers to use
556     * @param userPopulations The list of applicable user populations
557     * @return true if credentialproviders can be used
558     */
559    protected boolean _hasCredentialProviders(List<UserPopulation> userPopulations)
560    {
561        // Is there only one population or all populations have the same credential provider list as the first one?
562        if (userPopulations.size() == 1
563            || userPopulations.stream().map(UserPopulation::getCredentialProviders).distinct().count() == 1
564                && userPopulations.stream().map(this::_needsResetLinkOnFormCredential).distinct().count() == 1)
565        {
566            return true;
567        }
568
569        // Cannot determine the list
570        return false;
571    }
572    
573    /**
574     * Check if the user population include a {@link FormCredentialProvider}
575     * that requires display of a reset password link
576     * @param userPopulation the user population
577     * @return true if a reset password link needs to be displayed
578     */
579    private boolean _needsResetLinkOnFormCredential(UserPopulation userPopulation)
580    {
581        if (userPopulation.getCredentialProviders().stream()
582            .filter(FormCredentialProvider.class::isInstance)
583            .map(FormCredentialProvider.class::cast)
584            .map(FormCredentialProvider::displayResetLink)
585            .findAny().orElse(false))
586        {
587            return userPopulation.getUserDirectories().stream()
588                    .anyMatch(ModifiableUserDirectory.class::isInstance);
589        }
590        return false;
591    }
592    
593    /**
594     * Get the available populations for the given contexts
595     * @param request The request
596     * @param contexts The contexts
597     * @return The non-null list of populations
598     */
599    protected Set<String> _getAvailableUserPopulationsIds(Request request, List<String> contexts)
600    {
601        return _populationContextHelper.getUserPopulationsOnContexts(contexts, false, false);
602    }
603
604    /**
605     * Get the population for the given context
606     * @param request The request
607     * @param availableUserPopulations The available users populations
608     * @return The chosen population id. Can be null.
609     */
610    protected String _getChosenUserPopulationId(Request request, List<UserPopulation> availableUserPopulations)
611    {
612        // Get request population choice
613        String userPopulationId = request.getParameter(REQUEST_PARAMETER_POPULATION_NAME);
614        if (userPopulationId == null)
615        {
616            // Get memorized population choice
617            Session session = request.getSession(false);
618            if (session != null)
619            {
620                userPopulationId = (String) session.getAttribute(SESSION_CONNECTING_USERPOPULATION_ID);
621            }
622        }
623        
624        // A population choice was already made
625        if (StringUtils.isNotBlank(userPopulationId))
626        {
627            final String finalUserPopulationId = userPopulationId;
628            if (availableUserPopulations.stream().anyMatch(userPopulation -> userPopulation.getId().equals(finalUserPopulationId)))
629            {
630                return userPopulationId;
631            }
632            else
633            {
634                // Wrong submitted population id
635                request.setAttribute(REQUEST_ATTRIBUTE_INVALID_POPULATION, true);
636            }
637        }
638        
639        return null;
640    }
641    
642    /**
643     * Try to authenticate with this credential provider in this mode. Delegates to _doProcess
644     * @param request The request
645     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
646     * @param runningCredentialProvider the Credential provider to test
647     * @param runningCredentialProviderIndex The index of the currently tested credential provider
648     * @param redirector The cocoon redirector
649     * @param userPopulations The list of possible user populations
650     * @return false if we should try with another Credential provider, true otherwise
651     * @throws Exception If an error occurred
652     */
653    protected boolean _process(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, int runningCredentialProviderIndex, Redirector redirector, List<UserPopulation> userPopulations) throws Exception
654    {
655        boolean existingSession = request.getSession(false) != null;
656        _saveConnectingStateToSession(request, runningBlockingkMode ? -1 : runningCredentialProviderIndex, runningBlockingkMode);
657        if (_doProcess(request, runningBlockingkMode, runningCredentialProvider, redirector, userPopulations))
658        {
659            return true;
660        }
661        if (existingSession)
662        {
663            // A session was created but finally we do not need it
664            request.getSession().invalidate();
665        }
666        return false;
667    }
668    
669    /**
670     * Try to authenticate with this credential provider in this mode
671     * @param request The request
672     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
673     * @param runningCredentialProvider the Credential provider to test
674     * @param redirector The cocoon redirector
675     * @param userPopulations The list of possible user populations
676     * @return false if we should try with another Credential provider, true otherwise
677     * @throws Exception If an error occurred
678     */
679
680    protected boolean _doProcess(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, Redirector redirector, List<UserPopulation> userPopulations) throws Exception
681    {
682        if (runningCredentialProvider.grantAnonymousRequest(runningBlockingkMode))
683        {
684            // Anonymous request
685            request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
686            return true;
687        }
688        
689        UserIdentity potentialUserIdentity = null;
690        try
691        {
692            
693            potentialUserIdentity = runningCredentialProvider.getUserIdentity(runningBlockingkMode, redirector);
694        }
695        catch (WeakPasswordException e)
696        {
697            // User credentials are ok but user use a weak password
698            potentialUserIdentity = e.getUserIdentity();
699            _handleWeakPassword(request, runningCredentialProvider, redirector, potentialUserIdentity);
700        }
701        
702        if (redirector.hasRedirected())
703        {
704            // getCredentials require a redirection, save state and proceed
705            return true;
706        }
707        else if (potentialUserIdentity == null)
708        {
709            // Let us try another credential provider
710            return false;
711        }
712        
713        // Check if user exists
714        UserIdentity userIdentity = _getUserIdentity(userPopulations, potentialUserIdentity, redirector, runningBlockingkMode, runningCredentialProvider);
715        if (redirector.hasRedirected())
716        {
717            // getCredentials require a redirection, save state and proceed
718            return true;
719        }
720        else if (userIdentity == null)
721        {
722            // Let us try another credential provider
723            return false;
724        }
725
726        // Save user identity
727        _setUserIdentityInSession(request, userIdentity, runningCredentialProvider, runningBlockingkMode);
728        
729        // Authentication succeeded
730        runningCredentialProvider.userAllowed(runningBlockingkMode, userIdentity, redirector);
731        
732        if (_userStatusManager != null)
733        {
734            _userStatusManager.updateConnectionDate(userIdentity);
735        }
736        
737        _logLoginEvent(runningCredentialProvider, userIdentity);
738        
739        return true;
740    }
741    
742    /**
743     * Handle weak password exception
744     * @param request the request
745     * @param runningCredentialProvider the credential provider that detected the weak password
746     * @param redirector the redirector
747     * @param userIdentity the user identity with a weak password
748     * @throws Exception if an error occurred
749     */
750    protected void _handleWeakPassword(Request request, CredentialProvider runningCredentialProvider, Redirector redirector, UserIdentity userIdentity) throws Exception
751    {
752        ForensicLogger.info("authentication.form.weak.password", Map.of("userIdentity", userIdentity), userIdentity);
753        Optional<String> resetPasswordURI = _getWeakPasswordURI(request, userIdentity);
754        if (resetPasswordURI.isPresent())
755        {
756            // Force redirect to weak password url to force change password
757            getLogger().info("Password of user " + userIdentity + " does not meet the security requirements. Force to change password.");
758            redirector.redirect(false, resetPasswordURI.get());
759            return;
760        }
761        // Defaut implementation only log that user has a weak password. User will be authenticated
762        getLogger().warn("Password of user " + userIdentity + " does not meet the security requirements. User is authenticated despite the risk for security.");
763    }
764    
765    /**
766     * Get the URI where the user should be redirected after a weak password is detected
767     * @param request the current request
768     * @param userIdentity the user identity with a weak password
769     * @return the absolute uri
770     */
771    protected Optional<String> _getWeakPasswordURI(Request request, UserIdentity userIdentity)
772    {
773        // do not force password change in safe or maintenance mode
774        if (_userPasswordManager != null && RuntimeServlet.getRunMode() == RunMode.NORMAL)
775        {
776            return _userPasswordManager.getChangePasswordURI(request, userIdentity, true);
777        }
778        return Optional.empty();
779    }
780
781    /**
782     * Log login event
783     * @param credentialProvider the running credential provider
784     * @param userIdentity the user identity
785     */
786    protected void _logLoginEvent(CredentialProvider credentialProvider, UserIdentity userIdentity)
787    {
788        Map<String, Object> loginArgs = Map.of("credential-provider", credentialProvider.getCredentialProviderModelId(), "user", userIdentity);
789        ForensicLogger.info("authentication.login", loginArgs, userIdentity);
790    }
791    
792    /**
793     * Log logout event
794     * @param userIdentity the user identity
795     */
796    protected void _logLogoutEvent(UserIdentity userIdentity)
797    {
798        Map<String, Object> logoutArgs = Map.of("user", userIdentity);
799        ForensicLogger.info("authentication.logout", logoutArgs, userIdentity);
800    }
801    
802    /**
803     * Reset the connecting information in session
804     * @param request The request
805     */
806    protected static void _resetConnectingStateToSession(Request request)
807    {
808        Session session = request.getSession(false);
809        if (session != null)
810        {
811            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
812            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
813            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN);
814            session.removeAttribute(SESSION_CONNECTING_USERPOPULATION_ID);
815        }
816    }
817    
818    /**
819     * When the process end successfully, save the state
820     * @param request The request
821     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
822     * @param runningCredentialProviderIndex the currently tested credential provider
823     */
824    protected void _saveConnectingStateToSession(Request request, int runningCredentialProviderIndex, boolean runningBlockingkMode)
825    {
826        Session session = request.getSession(true);
827        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, runningCredentialProviderIndex);
828        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE, runningBlockingkMode);
829        session.setAttribute(SESSION_CONNECTING_USERPOPULATION_ID, request.getAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID));
830    }
831
832    /**
833     * Save user identity in request
834     * @param request The request
835     * @param userIdentity The useridentity to save
836     * @param credentialProvider The credential provider used to connect
837     * @param blockingMode The mode used for the credential provider
838     */
839    protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
840    {
841        setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode);
842        if (_observationManager != null)
843        {
844            Map<String, Object> eventParams = new HashMap<>();
845            eventParams.put(ObservationConstants.ARGS_USER, userIdentity);
846            _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams));
847        }
848    }
849    
850    /**
851     * Save user identity in request
852     * @param request The request
853     * @param userIdentity The useridentity to save
854     * @param credentialProvider The credential provider used to connect
855     * @param blockingMode The mode used for the credential provider
856     */
857    public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
858    {
859        Session session = renewSession(request);
860        _resetConnectingStateToSession(request);
861        session.setAttribute(SESSION_USERIDENTITY, userIdentity);
862        session.setAttribute(SESSION_CREDENTIALPROVIDER, credentialProvider);
863        session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE, blockingMode);
864    }
865    
866    /**
867     * Change the session id (for security purposes)
868     * @param request The current request
869     * @return The new session
870     */
871    public static Session renewSession(Request request)
872    {
873        Session session = request.getSession(true);
874        
875        Map<String, Object> attributesCopy = new HashMap<>();
876        
877        Enumeration<String> attributeNames = session.getAttributeNames();
878        while (attributeNames.hasMoreElements())
879        {
880            String attributeName = attributeNames.nextElement();
881            attributesCopy.put(attributeName, session.getAttribute(attributeName));
882        }
883        
884        session.invalidate();
885        
886        session = request.getSession(true);
887        
888        for (Entry<String, Object> attribute : attributesCopy.entrySet())
889        {
890            session.setAttribute(attribute.getKey(), attribute.getValue());
891        }
892        
893        return session;
894    }
895
896    /**
897     * Get the user identity of the connected user from the session
898     * @param request The request
899     * @return The connected useridentity or null
900     */
901    protected UserIdentity _getUserIdentityFromSession(Request request)
902    {
903        return getUserIdentityFromSession(request);
904    }
905    
906    /**
907     * Get the user identity of the connected user from the session
908     * @param request The request
909     * @return The connected useridentity or null
910     */
911    public static UserIdentity getUserIdentityFromSession(Request request)
912    {
913        Session session = request.getSession(false);
914        if (session != null)
915        {
916            return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY);
917        }
918        return null;
919    }
920   
921    /**
922     * Get the credential provider used for the current connection
923     * @param request The request
924     * @return The credential provider used or null
925     */
926    protected CredentialProvider _getCredentialProviderFromSession(Request request)
927    {
928        return getCredentialProviderFromSession(request);
929    }
930    
931    /**
932     * Get the credential provider used for the current connection
933     * @param request The request
934     * @return The credential provider used or null
935     */
936    public static CredentialProvider getCredentialProviderFromSession(Request request)
937    {
938        Session session = request.getSession(false);
939        if (session != null)
940        {
941            return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER);
942        }
943        return null;
944    }
945    
946    /**
947     * Get the credential provider mode used for the current connection
948     * @param request The request
949     * @return The credential provider mode used or null
950     */
951    protected Boolean _getCredentialProviderModeFromSession(Request request)
952    {
953        return getCredentialProviderModeFromSession(request);
954    }
955    
956    /**
957     * Get the credential provider mode used for the current connection
958     * @param request The request
959     * @return The credential provider mode used or null
960     */
961    public static Boolean getCredentialProviderModeFromSession(Request request)
962    {
963        Session session = request.getSession(false);
964        if (session != null)
965        {
966            return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE);
967        }
968        return null;
969    }
970
971    /**
972     * If there is a running credential provider, was it in non-blocking or blocking mode?
973     * @param request The request
974     * @return false if non-blocking, true if blocking
975     */
976    protected boolean _isCurrentCredentialProviderInBlockingMode(Request request)
977    {
978        if (StringUtils.equals(request.getParameter(REQUEST_PARAMETER_NONBLOCING), "force"))
979        {
980            return false;
981        }
982        
983        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
984        if (requestedCredentialParameterIndex != null && requestedCredentialParameterIndex != -1)
985        {
986            return true;
987        }
988        
989        Session session = request.getSession(false);
990        if (session != null)
991        {
992            Boolean mode = (Boolean) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
993            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
994            if (mode != null)
995            {
996                return mode.booleanValue();
997            }
998        }
999        return false;
1000    }
1001    
1002    /**
1003     * Call this to skip the currently used credential provider and proceed to the next one.
1004     * Useful for non blocking
1005     * @param request The request
1006     */
1007    public static void skipCurrentCredentialProvider(Request request)
1008    {
1009        Session session = request.getSession();
1010        if (session != null)
1011        {
1012            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1013            if (cpIndex != null)
1014            {
1015                cpIndex++;
1016                session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, cpIndex);
1017            }
1018        }
1019    }
1020
1021    /**
1022     * Get the current credential provider index or -1 if there no running provider FROM REQUEST PARAMETER
1023     * @param request The request
1024     * @return The credential provider index to use in the availablesCredentialProviders list or -1 or null
1025     */
1026    protected Integer _getCurrentCredentialProviderIndexFromParameter(Request request)
1027    {
1028        // Is the CP requested?
1029        String requestedCredentialParameterIndex = request.getParameter(REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX);
1030        if (StringUtils.isNotBlank(requestedCredentialParameterIndex))
1031        {
1032            int index = Integer.parseInt(requestedCredentialParameterIndex);
1033            return index;
1034        }
1035        return null;
1036    }
1037    
1038    /**
1039     * Get the current credential provider index or -1 if there no running provider
1040     * @param request The request
1041     * @param availableCredentialProviders The list of available credential provider
1042     * @return The credential provider index to use in the availablesCredentialProviders list or -1
1043     */
1044    protected int _getCurrentCredentialProviderIndex(Request request, List<CredentialProvider> availableCredentialProviders)
1045    {
1046        // Is the CP requested?
1047        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
1048        if (requestedCredentialParameterIndex != null)
1049        {
1050            if (requestedCredentialParameterIndex < availableCredentialProviders.size())
1051            {
1052                return requestedCredentialParameterIndex;
1053            }
1054            else
1055            {
1056                return -1;
1057            }
1058        }
1059        
1060        // Was the CP memorized?
1061        Session session = request.getSession(false);
1062        if (session != null)
1063        {
1064            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1065            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1066            
1067            if (cpIndex != null)
1068            {
1069                return cpIndex;
1070            }
1071        }
1072        
1073        // Default value
1074        return -1;
1075    }
1076    
1077    /**
1078     * Get the authentication context
1079     * @param request The request
1080     * @param parameters The action parameters
1081     * @return The context
1082     * @throws IllegalArgumentException If there is no context set
1083     */
1084    protected List<String> _getContexts(Request request, Parameters parameters)
1085    {
1086        String context = parameters.getParameter("context", null);
1087        if (context == null)
1088        {
1089            throw new IllegalArgumentException("The authentication is not parameterized correctly: an authentication context must be specified");
1090        }
1091        return Collections.singletonList(context);
1092    }
1093
1094    /**
1095     * Determine if the request is internal and do not need authentication
1096     * @param request The request
1097     * @return true to bypass this authentication
1098     */
1099    protected boolean _internalRequest(Request request)
1100    {
1101        return "true".equals(request.getAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED)) || request.getAttribute(REQUEST_ATTRIBUTE_INTERNAL_ALLOWED) != null;
1102    }
1103    
1104    /**
1105     * Determine if the request is one of the authentication process (except the credential providers)
1106     * @param request The request
1107     * @return true to bypass this authentication
1108     */
1109    protected boolean _acceptedUrl(Request request)
1110    {
1111        // URL without server context and leading slash.
1112        String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL);
1113        for (Pattern pattern : _acceptedUrlPatterns)
1114        {
1115            if (pattern.matcher(url).matches())
1116            {
1117                // Anonymous request
1118                request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
1119
1120                return true;
1121            }
1122        }
1123        
1124        return false;
1125    }
1126
1127    /**
1128     * This method ensure that there is a currently connected user and that it is still valid
1129     * @param request The request
1130     * @param redirector The cocoon redirector
1131     * @param parameters The action parameters
1132     * @return true if the user is connected and valid
1133     * @throws Exception if an error occurred
1134     */
1135    protected boolean _validateCurrentlyConnectedUser(Request request, Redirector redirector, Parameters parameters) throws Exception
1136    {
1137        Session session = request.getSession(false);
1138        UserIdentity userCurrentlyConnected = _getUserIdentityFromSession(request);
1139        CredentialProvider runningCredentialProvider = _getCredentialProviderFromSession(request);
1140        Boolean runningBlockingkMode = _getCredentialProviderModeFromSession(request);
1141        
1142        if (runningCredentialProvider == null || userCurrentlyConnected == null || runningBlockingkMode == null || !runningCredentialProvider.isStillConnected(runningBlockingkMode, userCurrentlyConnected, redirector))
1143        {
1144            if (redirector.hasRedirected())
1145            {
1146                return true;
1147            }
1148            
1149            // There is an invalid connected user
1150            if (session != null && userCurrentlyConnected != null)
1151            {
1152                session.invalidate();
1153            }
1154            return false;
1155        }
1156        
1157        // let us make an exception for the user image url since we need it on the 403 page
1158        if (RuntimeServlet.getRunMode() == RunMode.MAINTENANCE && MaintenanceAction.acceptedUrl(request))
1159        {
1160            return true;
1161        }
1162        
1163        _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userCurrentlyConnected, request, parameters);
1164        
1165        return true;
1166    }
1167    
1168    /**
1169     * This method is the second part of the process that ensure that there is a currently connected user and that it is still valid
1170     * @param userCurrentlyConnected The user to test
1171     * @param request The request
1172     * @param parameters The action parameters
1173     */
1174    protected void _validateCurrentlyConnectedUserIsInAuthorizedPopulation(UserIdentity userCurrentlyConnected, Request request, Parameters parameters)
1175    {
1176        if (_getTokenMode(parameters) == TOKEN_MODE.DEFAULT)
1177        {
1178            // we know this is a valid user, but we need to check if the context is correct
1179            List<String> contexts = _getContexts(request, parameters);
1180            // All user populations for this context
1181            Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, contexts);
1182            
1183            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1184            {
1185                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.");
1186            }
1187        }
1188        else
1189        {
1190            // In 'token only' mode, check if user is part of the active populations (regardless of the context)
1191            List<String> availableUserPopulationsIds = _userPopulationDAO.getEnabledUserPopulations(false).stream().map(UserPopulation::getId).collect(Collectors.toList());
1192            
1193            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1194            {
1195                throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated because its populations does not exist or it is disabled.");
1196            }
1197        }
1198    }
1199    
1200    /**
1201     * Test if user wants to logout and handle it
1202     * @param redirector The cocoon redirector
1203     * @param objectModel The cocoon object model
1204     * @param source The sitemap source
1205     * @param parameters The sitemap parameters
1206     * @return true if the user was logged out
1207     * @throws Exception if an error occurred
1208     */
1209    protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
1210    {
1211        Request request = ObjectModelHelper.getRequest(objectModel);
1212        if (StringUtils.equals(request.getContextPath() + request.getAttribute(WorkspaceMatcher.WORKSPACE_URI) + "/logout.html", request.getRequestURI())
1213                || StringUtils.equals("true", parameters.getParameter("logout", "false")))
1214        {
1215            // The user logs out
1216            UserIdentity currentUser = _currentUserProvider.getUser();
1217            if (currentUser != null) // user can be null when calling the url with an expired session
1218            {
1219                _currentUserProvider.logout(redirector);
1220                _logLogoutEvent(currentUser);
1221            }
1222            
1223            if (!redirector.hasRedirected())
1224            {
1225                redirector.redirect(false, getLogoutURL(request));
1226            }
1227            
1228            return true;
1229        }
1230        return false;
1231    }
1232    
1233    /**
1234     * Check the authentications of the authentication manager
1235     * @param userPopulations The list of available matching populations
1236     * @param redirector The cocoon redirector
1237     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
1238     * @param runningCredentialProvider The Credential provider to test
1239     * @param potentialUserIdentity A possible user identity. Population can be null. User may not exist either.
1240     * @return The user population matching credentials or null
1241     * @throws Exception If an error occurred
1242     * @throws AccessDeniedException If the user is rejected
1243     */
1244    protected UserIdentity _getUserIdentity(List<UserPopulation> userPopulations, UserIdentity potentialUserIdentity, Redirector redirector, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider) throws Exception
1245    {
1246        if (potentialUserIdentity.getPopulationId() == null)
1247        {
1248            for (UserPopulation up : userPopulations)
1249            {
1250                User user = _userManager.getUser(up, potentialUserIdentity.getLogin());
1251                if (_isLoginCaseExact(user, potentialUserIdentity))
1252                {
1253                    return user.getIdentity();
1254                }
1255            }
1256        }
1257        else
1258        {
1259            User user = _userManager.getUser(potentialUserIdentity.getPopulationId(), potentialUserIdentity.getLogin());
1260            if (_isLoginCaseExact(user, potentialUserIdentity))
1261            {
1262                return user.getIdentity();
1263            }
1264        }
1265        
1266        runningCredentialProvider.userNotAllowed(runningBlockingkMode, redirector);
1267        
1268        if (getLogger().isWarnEnabled())
1269        {
1270            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.");
1271        }
1272        
1273        return null;
1274    }
1275    
1276    private boolean _isLoginCaseExact(User user, UserIdentity potentialUserIdentity)
1277    {
1278        return user != null
1279                && (user.getUserDirectory().isCaseSensitive() && StringUtils.equals(user.getIdentity().getLogin(), potentialUserIdentity.getLogin())
1280                    || !user.getUserDirectory().isCaseSensitive() && StringUtils.equalsIgnoreCase(user.getIdentity().getLogin(), potentialUserIdentity.getLogin()));
1281    }
1282}