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