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