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.user.status.UserStatusManager;
061import org.ametys.core.util.URIUtils;
062import org.ametys.plugins.core.impl.authentication.FormCredentialProvider;
063import org.ametys.plugins.core.user.UserDAO;
064import org.ametys.plugins.core.user.management.UserPasswordManager;
065import org.ametys.runtime.authentication.AccessDeniedException;
066import org.ametys.runtime.authentication.AuthorizationRequiredException;
067import org.ametys.runtime.maintenance.MaintenanceAction;
068import org.ametys.runtime.servlet.RuntimeServlet;
069import org.ametys.runtime.servlet.RuntimeServlet.RunMode;
070import org.ametys.runtime.workspace.WorkspaceMatcher;
071
072/**
073 * Cocoon action to perform authentication.<br>
074 * The {@link CredentialProvider} define the authentication method and retrieves {@link Credentials}.<br>
075 * Finally, the Users instance extract the Principal corresponding to the {@link Credentials}.
076 */
077public class AuthenticateAction extends ServiceableAction implements ThreadSafe, Initializable
078{
079    /** The request attribute to allow internal action from an internal request. */
080    public static final String REQUEST_ATTRIBUTE_INTERNAL_ALLOWED = "Runtime:InternalAllowedRequest";
081    
082    /** The request attribute meaning that the request was not authenticated but granted */
083    public static final String REQUEST_ATTRIBUTE_GRANTED = "Runtime:GrantedRequest";
084    /** The request attribute name for transmitting the list of user populations */
085    public static final String REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST = "Runtime:UserPopulationsList";
086    /** The request attribute name for transmitting the currently chosen user population */
087    public static final String REQUEST_ATTRIBUTE_USER_POPULATION_ID = "Runtime:CurrentUserPopulationId";
088    /** The request attribute name for transmitting the login page url */
089    public static final String REQUEST_ATTRIBUTE_LOGIN_URL = "Runtime:RequestLoginURL";
090    
091    /** The session attribute name for storing the identity of the connected user */
092    public static final String SESSION_USERIDENTITY = "Runtime:UserIdentity";
093
094    /** Name of the user population HTML field */
095    public static final String REQUEST_PARAMETER_POPULATION_NAME = "UserPopulation";
096    /** Name of the credential provider index HTML field */
097    public static final String REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX = "CredentialProviderIndex";
098    /** Name of a parameter to change non blocking CP behavior */
099    public static final String REQUEST_PARAMETER_NONBLOCING = "NonBlocking";
100    
101    /** The request attribute name for indicating that the authentication process has been made. */
102    public static final String REQUEST_ATTRIBUTE_AUTHENTICATED = "Runtime:RequestAuthenticated";
103
104    /** The request parameter holding the token */
105    public static final String REQUEST_PARAMETER_TOKEN = "token";
106    /** The request parameter holding the token context */
107    public static final String REQUEST_PARAMETER_TOKEN_CONTEXT = "tokenContext";
108    /** The header parameter that can be set to handle the token */
109    public static final String HEADER_TOKEN = "X-Ametys-Token";
110
111    /** The sitemap parameter holding the token */
112    protected static final String PARAMETERS_PARAMETER_TOKEN = "token";
113    /** The sitemap parameter holding the token context */
114    protected static final String PARAMETERS_PARAMETER_TOKEN_CONTEXT = "tokenContext";
115    /** The request attribute name for transmitting a boolean that tell if there is a list of credential provider to choose */
116    protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST = "Runtime:RequestListCredentialProvider";
117    /** The request attribute name for transmitting the index in the list of chosen credential provider */
118    protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX = "Runtime:RequestCredentialProviderIndex";
119    /** The request attribute name to know if user population list should be proposed */
120    protected static final String REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST = "Runtime:UserPopulationsListDisplay";
121    /** The request attribute name for transmitting the potential list of user populations to the login screen . */
122    protected static final String REQUEST_ATTRIBUTE_INVALID_POPULATION = "Runtime:RequestInvalidPopulation";
123    /** The request attribute name for transmitting the list of contexts */
124    protected static final String REQUEST_ATTRIBUTE_CONTEXTS = "Runtime:Contexts";
125
126    /** The session attribute name for storing the credential provider index of the authentication (during connection process) */
127    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX = "Runtime:ConnectingCredentialProviderIndex";
128    /** The session attribute name for storing the last known credential provider index of the authentication (during connection process)*/
129    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN = "Runtime:ConnectingCredentialProviderIndexLastKnown";
130    /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=&gt;false, blocking=&gt;true (during connection process) */
131    protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_MODE = "Runtime:ConnectingCredentialProviderMode";
132    /** The session attribute name for storing the id of the user population (during connection process) */
133    protected static final String SESSION_CONNECTING_USERPOPULATION_ID = "Runtime:ConnectingUserPopulationId";
134    
135    /** The session attribute name for storing the credential provider of the authentication */
136    protected static final String SESSION_CREDENTIALPROVIDER = "Runtime:CredentialProvider";
137    /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=&gt;false, blocking=&gt;true */
138    protected static final String SESSION_CREDENTIALPROVIDER_MODE = "Runtime:CredentialProviderMode";
139
140    /** The sitemap parameter to set the token mode of the action */
141    protected static final String SITEMAP_PARAMETER_TOKEN_MODE = "token-mode";
142    
143    /** The DAO for user populations */
144    protected UserPopulationDAO _userPopulationDAO;
145    /** The user manager */
146    protected UserManager _userManager;
147    /** The helper for the associations population/context */
148    protected PopulationContextHelper _populationContextHelper;
149    /** The current user provider */
150    protected CurrentUserProvider _currentUserProvider;
151    
152    /** url requires for authentication */
153    protected Collection<Pattern> _acceptedUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/core/authenticate/[0-9]+$"), Pattern.compile("^plugins/core/reset-password.html$")});
154
155    /** The authentication token manager */
156    protected AuthenticationTokenManager _authenticateTokenManager;
157    /** The observation manager */
158    protected ObservationManager _observationManager;
159    /** The user account manager */
160    protected UserPasswordManager _userPasswordManager;
161    /** The user status manager */
162    protected UserStatusManager _userStatusManager;
163
164    /**
165     * The token mode of this authentication action
166     */
167    protected enum TOKEN_MODE
168    {
169        /** In this mode, only the token will be taken in account. If no token is found, authentication will not be considered done */
170        TOKEN_ONLY,
171        /** 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 */
172        ALLOW_ANONYMOUS,
173        /** In this default mode, the token will be taken in account, but if no token is found, the authentication process will continue */
174        DEFAULT
175    }
176    
177    @Override
178    public void initialize() throws Exception
179    {
180        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
181        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
182        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
183        _userStatusManager = (UserStatusManager) manager.lookup(UserStatusManager.ROLE);
184        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
185        try
186        {
187            _userPasswordManager = (UserPasswordManager) manager.lookup(UserPasswordManager.ROLE);
188            _authenticateTokenManager = (AuthenticationTokenManager) manager.lookup(AuthenticationTokenManager.ROLE);
189            _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
190        }
191        catch (ServiceException e)
192        {
193            // nothing... we are in safe mode, but this is not safe
194        }
195    }
196    
197    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
198    {
199        Request request = ObjectModelHelper.getRequest(objectModel);
200        
201        if (_preFlightCheck(redirector, resolver, objectModel, source, parameters) || _handleAuthenticationToken(request, parameters))
202        {
203            // We passed the authentication, let's mark it now
204            request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
205            
206            // We passed the authentication (with a user)
207            return EMPTY_MAP;
208        }
209        
210        // At this point, the user is still anonymous
211        
212        // If only token are authorized for authentication, stop authentication process. There is no user authenticated here.
213        if (_getTokenMode(parameters) != TOKEN_MODE.DEFAULT)
214        {
215            if (_getTokenMode(parameters) == TOKEN_MODE.ALLOW_ANONYMOUS)
216            {
217                request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
218            }
219            return null;
220        }
221        
222        // At this point, we already know that the entire process will be executed, whatever the outcome
223        // Set the flag, so that the authentication process won't repeat
224        request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true");
225        
226        // Get population and if possible credential providers
227        List<UserPopulation> chosenUserPopulations = new ArrayList<>();
228        List<CredentialProvider> credentialProviders = new ArrayList<>();
229        if (!_prepareUserPopulationsAndCredentialProviders(request, parameters, redirector, chosenUserPopulations, credentialProviders))
230        {
231            // Let's display the population screen
232            return EMPTY_MAP;
233        }
234        
235        // Get the currently running credential provider
236        int runningCredentialProviderIndex = _getCurrentCredentialProviderIndex(request, credentialProviders);
237        request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX, runningCredentialProviderIndex);
238        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_URL, getLoginURL(request));
239        
240        // Let's process non-blocking
241        if (!_isCurrentCredentialProviderInBlockingMode(request))
242        {
243            // if there was no one running, let's start with the first one
244            runningCredentialProviderIndex = Math.max(0, runningCredentialProviderIndex);
245            
246            for (; runningCredentialProviderIndex < credentialProviders.size(); runningCredentialProviderIndex++)
247            {
248                CredentialProvider runningCredentialProvider = credentialProviders.get(runningCredentialProviderIndex);
249                if (_process(request, false, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations))
250                {
251                    // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment
252                    return EMPTY_MAP;
253                }
254            }
255            
256            // No one matches
257            runningCredentialProviderIndex = -1;
258        }
259        
260        _saveLastKnownBlockingCredentialProvider(request, runningCredentialProviderIndex);
261        
262        // Let's process the current blocking one or the only existing one
263        if (_shouldRunFirstBlockingCredentialProvider(runningCredentialProviderIndex, credentialProviders, request, chosenUserPopulations))
264        {
265            CredentialProvider runningCredentialProvider = runningCredentialProviderIndex == -1 ? _getFirstBlockingCredentialProvider(credentialProviders) : credentialProviders.get(runningCredentialProviderIndex);
266            if (_process(request, true, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations))
267            {
268                // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment
269                return EMPTY_MAP;
270            }
271            
272            throw new AuthorizationRequiredException();
273        }
274        
275        // At this step we have two kind off requests
276        // 1) A secondary request of a blocking cp (such as captcha image...)
277        Integer formerRunningCredentialProviderIndex = (Integer) request.getSession(true).getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN);
278        if (formerRunningCredentialProviderIndex != null && credentialProviders.get(formerRunningCredentialProviderIndex).grantAnonymousRequest(true))
279        {
280            // Anonymous request
281            request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
282            _saveConnectingStateToSession(request, -1, true);
283            return EMPTY_MAP;
284        }
285        
286        // 2) Or a main stream request that should display the list of available blocking cp
287        return _displayBlockingList(redirector, request, credentialProviders);
288    }
289    
290    /**
291     * Prepare authentication
292     * @param redirector The redirector
293     * @param resolver The source resolver
294     * @param objectModel The object model
295     * @param source The source
296     * @param parameters The action parameters
297     * @return <code>true</code> if a user was authenticated, <code>false</code> otherwise
298     * @throws Exception if failed to prepare the authentication
299     */
300    protected boolean _preFlightCheck(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
301    {
302        Request request = ObjectModelHelper.getRequest(objectModel);
303        
304        return _handleLogout(redirector, objectModel, source, parameters)              // Test if user wants to logout
305                || _internalRequest(request)                                           // Test if this request was already authenticated or it the request is marked as an internal one
306                || _acceptedUrl(request)                                               // Test if the url is used for authentication
307                || _validateCurrentlyConnectedUser(request, redirector, parameters)    // Test if the currently connected user is still valid
308                || redirector.hasRedirected();
309    }
310    
311    /**
312     * Authenticate a user using the token in request (if configured so)
313     * @param request The request
314     * @param parameters The action parameters
315     * @return true if the user was authenticated
316     */
317    protected boolean _handleAuthenticationToken(Request request, Parameters parameters)
318    {
319        String token = request.getHeader(HEADER_TOKEN);
320        if (StringUtils.isBlank(token))
321        {
322            token = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN, _getTokenFromRequest(request));
323        }
324        
325        if (StringUtils.isNotBlank(token))
326        {
327            String context = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN_CONTEXT, null);
328            UserIdentity userIdentity = _validateToken(token, context);
329            if (userIdentity != null)
330            {
331                // Save user identity
332                _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true);
333                _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userIdentity, request, parameters);
334                
335                _userStatusManager.updateConnectionDate(userIdentity);
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        _userStatusManager.updateConnectionDate(userIdentity);
733        
734        _logLoginEvent(runningCredentialProvider, userIdentity);
735        
736        return true;
737    }
738    
739    /**
740     * Handle weak password exception
741     * @param request the request
742     * @param runningCredentialProvider the credential provider that detected the weak password
743     * @param redirector the redirector
744     * @param userIdentity the user identity with a weak password
745     * @throws Exception if an error occurred
746     */
747    protected void _handleWeakPassword(Request request, CredentialProvider runningCredentialProvider, Redirector redirector, UserIdentity userIdentity) throws Exception
748    {
749        ForensicLogger.info("authentication.form.weak.password", Map.of("userIdentity", userIdentity), userIdentity);
750        Optional<String> resetPasswordURI = _getWeakPasswordURI(request, userIdentity);
751        if (resetPasswordURI.isPresent())
752        {
753            // Force redirect to weak password url to force change password
754            getLogger().info("Password of user " + userIdentity + " does not meet the security requirements. Force to change password.");
755            redirector.redirect(false, resetPasswordURI.get());
756            return;
757        }
758        // Defaut implementation only log that user has a weak password. User will be authenticated
759        getLogger().warn("Password of user " + userIdentity + " does not meet the security requirements. User is authenticated despite the risk for security.");
760    }
761    
762    /**
763     * Get the URI where the user should be redirected after a weak password is detected
764     * @param request the current request
765     * @param userIdentity the user identity with a weak password
766     * @return the absolute uri
767     */
768    protected Optional<String> _getWeakPasswordURI(Request request, UserIdentity userIdentity)
769    {
770        // do not force password change in safe or maintenance mode
771        if (_userPasswordManager != null && RuntimeServlet.getRunMode() == RunMode.NORMAL)
772        {
773            return _userPasswordManager.getChangePasswordURI(request, userIdentity, true);
774        }
775        return Optional.empty();
776    }
777
778    /**
779     * Log login event
780     * @param credentialProvider the running credential provider
781     * @param userIdentity the user identity
782     */
783    protected void _logLoginEvent(CredentialProvider credentialProvider, UserIdentity userIdentity)
784    {
785        Map<String, Object> loginArgs = Map.of("credential-provider", credentialProvider.getCredentialProviderModelId(), "user", userIdentity);
786        ForensicLogger.info("authentication.login", loginArgs, userIdentity);
787    }
788    
789    /**
790     * Log logout event
791     * @param userIdentity the user identity
792     */
793    protected void _logLogoutEvent(UserIdentity userIdentity)
794    {
795        Map<String, Object> logoutArgs = Map.of("user", userIdentity);
796        ForensicLogger.info("authentication.logout", logoutArgs, userIdentity);
797    }
798    
799    /**
800     * Reset the connecting information in session
801     * @param request The request
802     */
803    protected static void _resetConnectingStateToSession(Request request)
804    {
805        Session session = request.getSession(false);
806        if (session != null)
807        {
808            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
809            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
810            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN);
811            session.removeAttribute(SESSION_CONNECTING_USERPOPULATION_ID);
812        }
813    }
814    
815    /**
816     * When the process end successfully, save the state
817     * @param request The request
818     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
819     * @param runningCredentialProviderIndex the currently tested credential provider
820     */
821    protected void _saveConnectingStateToSession(Request request, int runningCredentialProviderIndex, boolean runningBlockingkMode)
822    {
823        Session session = request.getSession(true);
824        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, runningCredentialProviderIndex);
825        session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE, runningBlockingkMode);
826        session.setAttribute(SESSION_CONNECTING_USERPOPULATION_ID, request.getAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID));
827    }
828
829    /**
830     * Save user identity in request
831     * @param request The request
832     * @param userIdentity The useridentity to save
833     * @param credentialProvider The credential provider used to connect
834     * @param blockingMode The mode used for the credential provider
835     */
836    protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
837    {
838        setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode);
839        if (_observationManager != null)
840        {
841            Map<String, Object> eventParams = new HashMap<>();
842            eventParams.put(ObservationConstants.ARGS_USER, userIdentity);
843            _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams));
844        }
845    }
846    
847    /**
848     * Save user identity in request
849     * @param request The request
850     * @param userIdentity The useridentity to save
851     * @param credentialProvider The credential provider used to connect
852     * @param blockingMode The mode used for the credential provider
853     */
854    public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
855    {
856        Session session = renewSession(request);
857        _resetConnectingStateToSession(request);
858        session.setAttribute(SESSION_USERIDENTITY, userIdentity);
859        session.setAttribute(SESSION_CREDENTIALPROVIDER, credentialProvider);
860        session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE, blockingMode);
861    }
862    
863    /**
864     * Change the session id (for security purposes)
865     * @param request The current request
866     * @return The new session
867     */
868    public static Session renewSession(Request request)
869    {
870        Session session = request.getSession(true);
871        
872        Map<String, Object> attributesCopy = new HashMap<>();
873        
874        Enumeration<String> attributeNames = session.getAttributeNames();
875        while (attributeNames.hasMoreElements())
876        {
877            String attributeName = attributeNames.nextElement();
878            attributesCopy.put(attributeName, session.getAttribute(attributeName));
879        }
880        
881        session.invalidate();
882        
883        session = request.getSession(true);
884        
885        for (Entry<String, Object> attribute : attributesCopy.entrySet())
886        {
887            session.setAttribute(attribute.getKey(), attribute.getValue());
888        }
889        
890        return session;
891    }
892
893    /**
894     * Get the user identity of the connected user from the session
895     * @param request The request
896     * @return The connected useridentity or null
897     */
898    protected UserIdentity _getUserIdentityFromSession(Request request)
899    {
900        return getUserIdentityFromSession(request);
901    }
902    
903    /**
904     * Get the user identity of the connected user from the session
905     * @param request The request
906     * @return The connected useridentity or null
907     */
908    public static UserIdentity getUserIdentityFromSession(Request request)
909    {
910        Session session = request.getSession(false);
911        if (session != null)
912        {
913            return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY);
914        }
915        return null;
916    }
917   
918    /**
919     * Get the credential provider used for the current connection
920     * @param request The request
921     * @return The credential provider used or null
922     */
923    protected CredentialProvider _getCredentialProviderFromSession(Request request)
924    {
925        return getCredentialProviderFromSession(request);
926    }
927    
928    /**
929     * Get the credential provider used for the current connection
930     * @param request The request
931     * @return The credential provider used or null
932     */
933    public static CredentialProvider getCredentialProviderFromSession(Request request)
934    {
935        Session session = request.getSession(false);
936        if (session != null)
937        {
938            return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER);
939        }
940        return null;
941    }
942    
943    /**
944     * Get the credential provider mode used for the current connection
945     * @param request The request
946     * @return The credential provider mode used or null
947     */
948    protected Boolean _getCredentialProviderModeFromSession(Request request)
949    {
950        return getCredentialProviderModeFromSession(request);
951    }
952    
953    /**
954     * Get the credential provider mode used for the current connection
955     * @param request The request
956     * @return The credential provider mode used or null
957     */
958    public static Boolean getCredentialProviderModeFromSession(Request request)
959    {
960        Session session = request.getSession(false);
961        if (session != null)
962        {
963            return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE);
964        }
965        return null;
966    }
967
968    /**
969     * If there is a running credential provider, was it in non-blocking or blocking mode?
970     * @param request The request
971     * @return false if non-blocking, true if blocking
972     */
973    protected boolean _isCurrentCredentialProviderInBlockingMode(Request request)
974    {
975        if (StringUtils.equals(request.getParameter(REQUEST_PARAMETER_NONBLOCING), "force"))
976        {
977            return false;
978        }
979        
980        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
981        if (requestedCredentialParameterIndex != null && requestedCredentialParameterIndex != -1)
982        {
983            return true;
984        }
985        
986        Session session = request.getSession(false);
987        if (session != null)
988        {
989            Boolean mode = (Boolean) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
990            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE);
991            if (mode != null)
992            {
993                return mode.booleanValue();
994            }
995        }
996        return false;
997    }
998    
999    /**
1000     * Call this to skip the currently used credential provider and proceed to the next one.
1001     * Useful for non blocking
1002     * @param request The request
1003     */
1004    public static void skipCurrentCredentialProvider(Request request)
1005    {
1006        Session session = request.getSession();
1007        if (session != null)
1008        {
1009            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1010            if (cpIndex != null)
1011            {
1012                cpIndex++;
1013                session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, cpIndex);
1014            }
1015        }
1016    }
1017
1018    /**
1019     * Get the current credential provider index or -1 if there no running provider FROM REQUEST PARAMETER
1020     * @param request The request
1021     * @return The credential provider index to use in the availablesCredentialProviders list or -1 or null
1022     */
1023    protected Integer _getCurrentCredentialProviderIndexFromParameter(Request request)
1024    {
1025        // Is the CP requested?
1026        String requestedCredentialParameterIndex = request.getParameter(REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX);
1027        if (StringUtils.isNotBlank(requestedCredentialParameterIndex))
1028        {
1029            int index = Integer.parseInt(requestedCredentialParameterIndex);
1030            return index;
1031        }
1032        return null;
1033    }
1034    
1035    /**
1036     * Get the current credential provider index or -1 if there no running provider
1037     * @param request The request
1038     * @param availableCredentialProviders The list of available credential provider
1039     * @return The credential provider index to use in the availablesCredentialProviders list or -1
1040     */
1041    protected int _getCurrentCredentialProviderIndex(Request request, List<CredentialProvider> availableCredentialProviders)
1042    {
1043        // Is the CP requested?
1044        Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request);
1045        if (requestedCredentialParameterIndex != null)
1046        {
1047            if (requestedCredentialParameterIndex < availableCredentialProviders.size())
1048            {
1049                return requestedCredentialParameterIndex;
1050            }
1051            else
1052            {
1053                return -1;
1054            }
1055        }
1056        
1057        // Was the CP memorized?
1058        Session session = request.getSession(false);
1059        if (session != null)
1060        {
1061            Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1062            session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX);
1063            
1064            if (cpIndex != null)
1065            {
1066                return cpIndex;
1067            }
1068        }
1069        
1070        // Default value
1071        return -1;
1072    }
1073    
1074    /**
1075     * Get the authentication context
1076     * @param request The request
1077     * @param parameters The action parameters
1078     * @return The context
1079     * @throws IllegalArgumentException If there is no context set
1080     */
1081    protected List<String> _getContexts(Request request, Parameters parameters)
1082    {
1083        String context = parameters.getParameter("context", null);
1084        if (context == null)
1085        {
1086            throw new IllegalArgumentException("The authentication is not parameterized correctly: an authentication context must be specified");
1087        }
1088        return Collections.singletonList(context);
1089    }
1090
1091    /**
1092     * Determine if the request is internal and do not need authentication
1093     * @param request The request
1094     * @return true to bypass this authentication
1095     */
1096    protected boolean _internalRequest(Request request)
1097    {
1098        return "true".equals(request.getAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED)) || request.getAttribute(REQUEST_ATTRIBUTE_INTERNAL_ALLOWED) != null;
1099    }
1100    
1101    /**
1102     * Determine if the request is one of the authentication process (except the credential providers)
1103     * @param request The request
1104     * @return true to bypass this authentication
1105     */
1106    protected boolean _acceptedUrl(Request request)
1107    {
1108        // URL without server context and leading slash.
1109        String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL);
1110        for (Pattern pattern : _acceptedUrlPatterns)
1111        {
1112            if (pattern.matcher(url).matches())
1113            {
1114                // Anonymous request
1115                request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
1116
1117                return true;
1118            }
1119        }
1120        
1121        return false;
1122    }
1123
1124    /**
1125     * This method ensure that there is a currently connected user and that it is still valid
1126     * @param request The request
1127     * @param redirector The cocoon redirector
1128     * @param parameters The action parameters
1129     * @return true if the user is connected and valid
1130     * @throws Exception if an error occurred
1131     */
1132    protected boolean _validateCurrentlyConnectedUser(Request request, Redirector redirector, Parameters parameters) throws Exception
1133    {
1134        Session session = request.getSession(false);
1135        UserIdentity userCurrentlyConnected = _getUserIdentityFromSession(request);
1136        CredentialProvider runningCredentialProvider = _getCredentialProviderFromSession(request);
1137        Boolean runningBlockingkMode = _getCredentialProviderModeFromSession(request);
1138        
1139        if (runningCredentialProvider == null || userCurrentlyConnected == null || runningBlockingkMode == null || !runningCredentialProvider.isStillConnected(runningBlockingkMode, userCurrentlyConnected, redirector))
1140        {
1141            if (redirector.hasRedirected())
1142            {
1143                return true;
1144            }
1145            
1146            // There is an invalid connected user
1147            if (session != null && userCurrentlyConnected != null)
1148            {
1149                session.invalidate();
1150            }
1151            return false;
1152        }
1153        
1154        // let us make an exception for the user image url since we need it on the 403 page
1155        if (RuntimeServlet.getRunMode() == RunMode.MAINTENANCE && MaintenanceAction.acceptedUrl(request))
1156        {
1157            return true;
1158        }
1159        
1160        _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userCurrentlyConnected, request, parameters);
1161        
1162        return true;
1163    }
1164    
1165    /**
1166     * This method is the second part of the process that ensure that there is a currently connected user and that it is still valid
1167     * @param userCurrentlyConnected The user to test
1168     * @param request The request
1169     * @param parameters The action parameters
1170     */
1171    protected void _validateCurrentlyConnectedUserIsInAuthorizedPopulation(UserIdentity userCurrentlyConnected, Request request, Parameters parameters)
1172    {
1173        if (_getTokenMode(parameters) == TOKEN_MODE.DEFAULT)
1174        {
1175            // we know this is a valid user, but we need to check if the context is correct
1176            List<String> contexts = _getContexts(request, parameters);
1177            // All user populations for this context
1178            Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, contexts);
1179            
1180            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1181            {
1182                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.");
1183            }
1184        }
1185        else
1186        {
1187            // In 'token only' mode, check if user is part of the active populations (regardless of the context)
1188            List<String> availableUserPopulationsIds = _userPopulationDAO.getEnabledUserPopulations(false).stream().map(UserPopulation::getId).collect(Collectors.toList());
1189            
1190            if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId()))
1191            {
1192                throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated because its populations does not exist or it is disabled.");
1193            }
1194        }
1195    }
1196    
1197    /**
1198     * Test if user wants to logout and handle it
1199     * @param redirector The cocoon redirector
1200     * @param objectModel The cocoon object model
1201     * @param source The sitemap source
1202     * @param parameters The sitemap parameters
1203     * @return true if the user was logged out
1204     * @throws Exception if an error occurred
1205     */
1206    protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
1207    {
1208        Request request = ObjectModelHelper.getRequest(objectModel);
1209        if (StringUtils.equals(request.getContextPath() + request.getAttribute(WorkspaceMatcher.WORKSPACE_URI) + "/logout.html", request.getRequestURI())
1210                || StringUtils.equals("true", parameters.getParameter("logout", "false")))
1211        {
1212            // The user logs out
1213            UserIdentity currentUser = _currentUserProvider.getUser();
1214            if (currentUser != null) // user can be null when calling the url with an expired session
1215            {
1216                _currentUserProvider.logout(redirector);
1217                _logLogoutEvent(currentUser);
1218            }
1219            
1220            if (!redirector.hasRedirected())
1221            {
1222                redirector.redirect(false, getLogoutURL(request));
1223            }
1224            
1225            return true;
1226        }
1227        return false;
1228    }
1229    
1230    /**
1231     * Check the authentications of the authentication manager
1232     * @param userPopulations The list of available matching populations
1233     * @param redirector The cocoon redirector
1234     * @param runningBlockingkMode false for non-blocking mode, true for blocking mode
1235     * @param runningCredentialProvider The Credential provider to test
1236     * @param potentialUserIdentity A possible user identity. Population can be null. User may not exist either.
1237     * @return The user population matching credentials or null
1238     * @throws Exception If an error occurred
1239     * @throws AccessDeniedException If the user is rejected
1240     */
1241    protected UserIdentity _getUserIdentity(List<UserPopulation> userPopulations, UserIdentity potentialUserIdentity, Redirector redirector, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider) throws Exception
1242    {
1243        if (potentialUserIdentity.getPopulationId() == null)
1244        {
1245            for (UserPopulation up : userPopulations)
1246            {
1247                User user = _userManager.getUser(up, potentialUserIdentity.getLogin());
1248                if (_isLoginCaseExact(user, potentialUserIdentity))
1249                {
1250                    return user.getIdentity();
1251                }
1252            }
1253        }
1254        else
1255        {
1256            User user = _userManager.getUser(potentialUserIdentity.getPopulationId(), potentialUserIdentity.getLogin());
1257            if (_isLoginCaseExact(user, potentialUserIdentity))
1258            {
1259                return user.getIdentity();
1260            }
1261        }
1262        
1263        runningCredentialProvider.userNotAllowed(runningBlockingkMode, redirector);
1264        
1265        if (getLogger().isWarnEnabled())
1266        {
1267            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.");
1268        }
1269        
1270        return null;
1271    }
1272    
1273    private boolean _isLoginCaseExact(User user, UserIdentity potentialUserIdentity)
1274    {
1275        return user != null
1276                && (user.getUserDirectory().isCaseSensitive() && StringUtils.equals(user.getIdentity().getLogin(), potentialUserIdentity.getLogin())
1277                    || !user.getUserDirectory().isCaseSensitive() && StringUtils.equalsIgnoreCase(user.getIdentity().getLogin(), potentialUserIdentity.getLogin()));
1278    }
1279}