001/*
002 *  Copyright 2020 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.plugins.mobileapp.action;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.nio.charset.StandardCharsets;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.function.Function;
026import java.util.stream.Collectors;
027
028import javax.servlet.http.HttpServletRequest;
029
030import org.apache.avalon.framework.parameters.ParameterException;
031import org.apache.avalon.framework.parameters.Parameters;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.cocoon.acting.ServiceableAction;
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Redirector;
037import org.apache.cocoon.environment.Request;
038import org.apache.cocoon.environment.SourceResolver;
039import org.apache.cocoon.environment.http.HttpEnvironment;
040import org.apache.commons.lang3.StringUtils;
041
042import org.ametys.core.authentication.AuthenticateAction;
043import org.ametys.core.authentication.CredentialProvider;
044import org.ametys.core.authentication.token.AuthenticationTokenManager;
045import org.ametys.core.cocoon.JSonReader;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.population.PopulationContextHelper;
048import org.ametys.core.user.population.UserPopulation;
049import org.ametys.core.user.population.UserPopulationDAO;
050import org.ametys.core.util.JSONUtils;
051import org.ametys.core.util.URIUtils;
052import org.ametys.plugins.core.impl.authentication.FormCredentialProvider;
053import org.ametys.runtime.authentication.AccessDeniedException;
054
055/**
056 * Returns the token for a user (only to be used by the mobile app on one site)
057 */
058public class GetTokenAction extends ServiceableAction
059{
060    /** Authentication Token Manager */
061    protected AuthenticationTokenManager _authenticationTokenManager;
062    
063    /** The user population DAO */
064    protected UserPopulationDAO _userPopulationDAO;
065
066    /** The helper for the associations population/context */
067    protected PopulationContextHelper _populationContextHelper;
068
069    /** The current user provider */
070    protected CurrentUserProvider _currentUserProvider;
071
072    /** JSON Utils */
073    protected JSONUtils _jsonUtils;
074    
075    @Override
076    public void service(ServiceManager smanager) throws ServiceException
077    {
078        super.service(smanager);
079        _authenticationTokenManager = (AuthenticationTokenManager) smanager.lookup(AuthenticationTokenManager.ROLE);
080
081        _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE);
082        _populationContextHelper = (PopulationContextHelper) smanager.lookup(PopulationContextHelper.ROLE);
083        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
084
085        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
086    }
087    
088    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
089    {
090        Map<String, Object> result = new HashMap<>();
091        Request request = ObjectModelHelper.getRequest(objectModel);
092        
093        
094        String body = "{}";
095        
096        if (_currentUserProvider.getUser() != null)
097        {
098            String generateToken = _authenticationTokenManager.generateToken(0, "mobileapp", "Token for the mobile app");
099            
100            result.put("code", 200);
101            result.put("token", generateToken);
102        }
103        else
104        {
105            HttpServletRequest postReq = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
106            try (InputStream postBody = postReq.getInputStream())
107            {
108                body = new String(postBody.readAllBytes(), StandardCharsets.UTF_8);
109            }
110            
111            Map<String, Object> jsonParams = _jsonUtils.convertJsonToMap(body);
112    
113            String login;
114            if (jsonParams.containsKey("login"))
115            {
116                login = (String) jsonParams.get("login");
117            }
118            else
119            {
120                login = request.getParameter("login");
121            }
122    
123            String password;
124            if (jsonParams.containsKey("password"))
125            {
126                password = (String) jsonParams.get("password");
127            }
128            else
129            {
130                password = request.getParameter("password");
131            }
132            boolean authenticated = false;
133            
134            if (StringUtils.isNotBlank(login))
135            {
136                authenticated = authenticate(login, password, request, resolver, parameters);
137            }
138    
139            if (authenticated)
140            {
141                String generateToken = _authenticationTokenManager.generateToken(0, "mobileapp", "Token for the mobile app");
142        
143                result.put("code", 200);
144                result.put("token", generateToken);
145                // Do not provide the redirector. We don't want the request to be redirected
146                // This shouldn't have an impact as the credential provider used to login was forced to be a FormCredentialProvider
147                _currentUserProvider.logout(null);
148            }
149            else
150            {
151                result.put("code", 403);
152                throw new AccessDeniedException();
153            }
154        }
155        
156        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
157        return EMPTY_MAP;
158    }
159    
160    private boolean authenticate(String login, String password, Request request, SourceResolver resolver, Parameters parameters) throws ParameterException
161    {
162        String siteName = parameters.getParameter("site");
163        List<String> populationContexts = List.of("/sites/" + siteName, "/sites-fo/" + siteName);
164        boolean atLeastOneAvailablePopulation = false;
165        
166        for (String context : populationContexts)
167        {
168        
169            Map<UserPopulation, List<CredentialProvider>> populations = _populationContextHelper.getUserPopulationsOnContexts(List.of(context), false, false).stream()
170                    .map(_userPopulationDAO::getUserPopulation)
171                    .collect(Collectors.toMap(Function.identity(), u -> u.getCredentialProviders()));
172            
173            
174            for (Entry<UserPopulation, List<CredentialProvider>> entry : populations.entrySet())
175            {
176                UserPopulation userPopulation = entry.getKey();
177                List<CredentialProvider> providers = entry.getValue();
178                for (int i = 0; i < providers.size(); i++)
179                {
180                    CredentialProvider credentialProvider = providers.get(i);
181                    if (credentialProvider instanceof FormCredentialProvider)
182                    {
183                        try
184                        {
185                            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, "false");
186                            String loginParameters = "Username=" + URIUtils.encodeParameter(login);
187                            loginParameters += "&Password=" + URIUtils.encodeParameter(password);
188                            loginParameters += "&UserPopulation=" + URIUtils.encodeParameter(userPopulation.getId());
189                            loginParameters += "&CredentialProviderIndex=" + i;
190                            loginParameters += "&context=" + URIUtils.encodeParameter(context);
191                            
192                            atLeastOneAvailablePopulation = true;
193                            
194                            resolver.resolveURI("cocoon:/authenticate?" + loginParameters);
195                            if (_currentUserProvider.getUser() != null)
196                            {
197                                break;
198                            }
199                        }
200                        catch (IOException e)
201                        {
202                            getLogger().error("Impossible to test logins on population '" + userPopulation.getId() + "' using credential provider at position '" + i + "'");
203                        }
204                    }
205                }
206            }
207            
208            if (!atLeastOneAvailablePopulation)
209            {
210                getLogger().error("Error while logging-in from the mobile application to the '" + siteName + "' site. At least one population should be configured with a form credential provider.");
211            }
212        }
213        
214        return _currentUserProvider.getUser() != null;
215    }
216
217}