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