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}