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