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            if (StringUtils.isNotBlank(login))
134            {
135                authenticated = authenticate(login, password, request, resolver, parameters);
136            }
137
138            if (authenticated)
139            {
140                String generateToken = _authenticationTokenManager.generateToken(0, "mobileapp", "Token for the mobile app");
141
142                result.put("code", 200);
143                result.put("token", generateToken);
144                // Do not provide the redirector. We don't want the request to be redirected
145                // This shouldn't have an impact as the credential provider used to login was forced to be a FormCredentialProvider
146                _currentUserProvider.logout(null);
147            }
148            else
149            {
150                result.put("code", 403);
151                throw new AccessDeniedException();
152            }
153        }
154
155        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
156        return EMPTY_MAP;
157    }
158
159    private boolean authenticate(String login, String password, Request request, SourceResolver resolver, Parameters parameters) throws ParameterException
160    {
161        String siteName = parameters.getParameter("site");
162        List<String> populationContexts = List.of("/sites/" + siteName, "/sites-fo/" + siteName);
163        boolean atLeastOneAvailablePopulation = false;
164
165        for (String context : populationContexts)
166        {
167
168            Map<UserPopulation, List<CredentialProvider>> populations = _populationContextHelper.getUserPopulationsOnContexts(List.of(context), false, false).stream()
169                    .map(_userPopulationDAO::getUserPopulation)
170                    .collect(Collectors.toMap(Function.identity(), u -> u.getCredentialProviders()));
171
172
173            for (Entry<UserPopulation, List<CredentialProvider>> entry : populations.entrySet())
174            {
175                UserPopulation userPopulation = entry.getKey();
176                List<CredentialProvider> providers = entry.getValue();
177                for (int i = 0; i < providers.size(); i++)
178                {
179                    CredentialProvider credentialProvider = providers.get(i);
180                    if (credentialProvider instanceof FormCredentialProvider)
181                    {
182                        try
183                        {
184                            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, "false");
185                            String loginParameters = "Username=" + URIUtils.encodeParameter(login);
186                            loginParameters += "&Password=" + URIUtils.encodeParameter(password);
187                            loginParameters += "&UserPopulation=" + URIUtils.encodeParameter(userPopulation.getId());
188                            loginParameters += "&CredentialProviderIndex=" + i;
189                            loginParameters += "&context=" + URIUtils.encodeParameter(context);
190
191                            atLeastOneAvailablePopulation = true;
192
193                            resolver.resolveURI("cocoon:/authenticate?" + loginParameters);
194                            if (_currentUserProvider.getUser() != null)
195                            {
196                                break;
197                            }
198                        }
199                        catch (IOException e)
200                        {
201                            getLogger().error("Impossible to test logins on population '" + userPopulation.getId() + "' using credential provider at position '" + i + "'");
202                        }
203                    }
204                }
205            }
206
207            if (!atLeastOneAvailablePopulation)
208            {
209                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.");
210            }
211        }
212
213        return _currentUserProvider.getUser() != null;
214    }
215
216}