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