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        if (_currentUserProvider.getUser() != null)
094        {
095            String generateToken = _authenticationTokenManager.generateToken(0, "mobileapp", "Token for the mobile app");
096
097            result.put("code", 200);
098            result.put("token", generateToken);
099        }
100        else
101        {
102            String body = null;
103            HttpServletRequest postReq = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
104            try (InputStream postBody = postReq.getInputStream())
105            {
106                body = new String(postBody.readAllBytes(), StandardCharsets.UTF_8);
107            }
108
109            Map<String, Object> jsonParams = _jsonUtils.convertJsonToMap(body);
110
111            String login = (String) getParameter("login", jsonParams, request);
112            String password = (String) getParameter("password", jsonParams, request);
113            String population = (String) getParameter("population", jsonParams, request);
114
115            boolean authenticated = false;
116            if (StringUtils.isNotBlank(login))
117            {
118                UserPopulation userPopulation = population != null ? _userPopulationDAO.getUserPopulation(population) : null;
119
120                if (userPopulation != null)
121                {
122                    authenticated = authenticate(login, password, request, resolver, parameters, userPopulation, userPopulation.getCredentialProviders());
123                }
124                else
125                {
126                    authenticated = authenticate(login, password, request, resolver, parameters);
127                }
128            }
129
130            if (authenticated)
131            {
132                String generateToken = _authenticationTokenManager.generateToken(0, "mobileapp", "Token for the mobile app");
133
134                result.put("code", 200);
135                result.put("token", generateToken);
136                // Do not provide the redirector. We don't want the request to be redirected
137                // This shouldn't have an impact as the credential provider used to login was forced to be a FormCredentialProvider
138                _currentUserProvider.logout(null);
139            }
140            else
141            {
142                result.put("code", 403);
143                throw new AccessDeniedException();
144            }
145        }
146
147        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
148        return EMPTY_MAP;
149    }
150    
151    private Object getParameter(String name, Map<String, Object> jsonParams, Request request)
152    {
153        if (jsonParams.containsKey(name))
154        {
155            return jsonParams.get(name);
156        }
157        else if (request.getParameter(name) != null)
158        {
159            return request.getParameter(name);
160        }
161        else
162        {
163            return request.getAttribute(name);
164        }
165    }
166
167    private boolean authenticate(String login, String password, Request request, SourceResolver resolver, Parameters parameters) throws ParameterException
168    {
169        String siteName = parameters.getParameter("site");
170        List<String> populationContexts = List.of("/sites/" + siteName, "/sites-fo/" + siteName);
171        boolean atLeastOneAvailablePopulation = false;
172
173        for (String context : populationContexts)
174        {
175
176            Map<UserPopulation, List<CredentialProvider>> populations = _populationContextHelper.getUserPopulationsOnContexts(List.of(context), false, false).stream()
177                    .map(_userPopulationDAO::getUserPopulation)
178                    .collect(Collectors.toMap(Function.identity(), u -> u.getCredentialProviders()));
179
180
181            for (Entry<UserPopulation, List<CredentialProvider>> entry : populations.entrySet())
182            {
183                atLeastOneAvailablePopulation = _tryConnect(login, password, request, resolver, context, entry.getKey(), entry.getValue()) || atLeastOneAvailablePopulation;
184            }
185
186            if (!atLeastOneAvailablePopulation)
187            {
188                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.");
189            }
190        }
191
192        return _currentUserProvider.getUser() != null;
193    }
194
195    private boolean _tryConnect(String login, String password, Request request, SourceResolver resolver, String context, UserPopulation userPopulation, List<CredentialProvider> providers)
196    {
197        boolean atLeastOneAvailablePopulation = false;
198        for (int i = 0; i < providers.size(); i++)
199        {
200            CredentialProvider credentialProvider = providers.get(i);
201            if (credentialProvider instanceof FormCredentialProvider)
202            {
203                try
204                {
205                    request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, "false");
206                    String loginParameters = "Username=" + URIUtils.encodeParameter(login);
207                    loginParameters += "&Password=" + URIUtils.encodeParameter(password);
208                    loginParameters += "&UserPopulation=" + URIUtils.encodeParameter(userPopulation.getId());
209                    loginParameters += "&CredentialProviderIndex=" + i;
210                    loginParameters += "&context=" + URIUtils.encodeParameter(context);
211
212                    atLeastOneAvailablePopulation = true;
213
214                    resolver.resolveURI("cocoon:/authenticate?" + loginParameters);
215                    if (_currentUserProvider.getUser() != null)
216                    {
217                        break;
218                    }
219                }
220                catch (IOException e)
221                {
222                    getLogger().error("Impossible to test logins on population '" + userPopulation.getId() + "' using credential provider at position '" + i + "'");
223                }
224            }
225        }
226        return atLeastOneAvailablePopulation;
227    }
228
229    private boolean authenticate(String login, String password, Request request, SourceResolver resolver, Parameters parameters, UserPopulation userPopulation, List<CredentialProvider> providers) throws ParameterException
230    {
231        String siteName = parameters.getParameter("site");
232        List<String> populationContexts = List.of("/sites/" + siteName, "/sites-fo/" + siteName);
233
234        for (String context : populationContexts)
235        {
236            _tryConnect(login, password, request, resolver, context, userPopulation, providers);
237        }
238
239        return _currentUserProvider.getUser() != null;
240    }
241}