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