001/* 002 * Copyright 2016 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.site; 017 018import java.io.InputStream; 019import java.nio.charset.StandardCharsets; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Enumeration; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Optional; 029import java.util.Set; 030import java.util.regex.Pattern; 031 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 035import org.apache.avalon.framework.parameters.Parameters; 036import org.apache.cocoon.environment.ObjectModelHelper; 037import org.apache.cocoon.environment.Redirector; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.environment.Session; 040import org.apache.cocoon.environment.http.HttpCookie; 041import org.apache.commons.lang3.StringUtils; 042import org.apache.commons.lang3.Strings; 043import org.apache.hc.client5.http.classic.methods.HttpGet; 044import org.apache.hc.client5.http.classic.methods.HttpPost; 045import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; 046import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; 047import org.apache.hc.core5.http.HttpEntity; 048import org.apache.hc.core5.http.HttpResponse; 049import org.apache.hc.core5.http.NameValuePair; 050import org.apache.hc.core5.http.message.BasicNameValuePair; 051import org.xml.sax.SAXException; 052 053import org.ametys.core.ObservationConstants; 054import org.ametys.core.authentication.AuthenticateAction; 055import org.ametys.core.authentication.CredentialProvider; 056import org.ametys.core.observation.Event; 057import org.ametys.core.user.UserIdentity; 058import org.ametys.core.user.population.UserPopulationDAO; 059import org.ametys.core.util.LambdaUtils.LambdaException; 060import org.ametys.plugins.core.user.UserDAO; 061import org.ametys.plugins.site.Site; 062import org.ametys.runtime.config.Config; 063import org.ametys.runtime.workspace.WorkspaceMatcher; 064 065/** 066 * The authenticate action for front side 067 */ 068public class FrontAuthenticateAction extends AuthenticateAction 069{ 070 /** url requires for authentication */ 071 protected Collection<Pattern> _acceptedSiteUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/site/authenticate/[0-9]+$")}); 072 073 @Override 074 protected boolean _acceptedUrl(Request request) 075 { 076 // URL without server context and leading slash. 077 String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL); 078 for (Pattern pattern : _acceptedSiteUrlPatterns) 079 { 080 if (pattern.matcher(url).matches()) 081 { 082 // Anonymous request 083 request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true); 084 085 return true; 086 } 087 } 088 089 return false; 090 } 091 092 @Override 093 protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode) 094 { 095 // Method overridden to call the correct static method, the default one would be the one from the parent class 096 setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode); 097 if (_observationManager != null) 098 { 099 Map<String, Object> eventParams = new HashMap<>(); 100 eventParams.put(ObservationConstants.ARGS_USER, userIdentity); 101 _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams)); 102 } 103 } 104 105 /** 106 * Save user identity in request 107 * @param request The request 108 * @param userIdentity The useridentity to save 109 * @param credentialProvider The credential provider used to connect 110 * @param blockingMode The mode used for the credential provider 111 */ 112 public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode) 113 { 114 Site site = (Site) request.getAttribute("site"); 115 String siteName = site.getName(); 116 117 Session session = renewSession(request); 118 _resetConnectingStateToSession(request); 119 session.setAttribute(SESSION_USERIDENTITY + "-" + siteName, userIdentity); 120 session.setAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName, credentialProvider); 121 session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName, blockingMode); 122 } 123 124 @Override 125 protected UserIdentity _getUserIdentityFromSession(Request request) 126 { 127 UserIdentity userIdentityFromSession = getUserIdentityFromSession(request); 128 if (userIdentityFromSession != null) 129 { 130 return userIdentityFromSession; 131 } 132 133 Session session = request.getSession(false); 134 if (session != null) 135 { 136 Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, _getContexts(request, null)); 137 138 // check if connected in the site application, not only on this site 139 userIdentityFromSession = super._getUserIdentityFromSession(request); 140 if (userIdentityFromSession != null && availableUserPopulationsIds.contains(userIdentityFromSession.getPopulationId())) 141 { 142 _setUserIdentityInSession(request, userIdentityFromSession, new UserDAO.ImpersonateCredentialProvider(), true); 143 return userIdentityFromSession; 144 } 145 146 Enumeration<String> attributeNames = session.getAttributeNames(); 147 while (attributeNames.hasMoreElements()) 148 { 149 String attributeName = attributeNames.nextElement(); 150 if (attributeName.startsWith(SESSION_USERIDENTITY + "-")) 151 { 152 UserIdentity userIdentity = (UserIdentity) session.getAttribute(attributeName); 153 154 if (availableUserPopulationsIds.contains(userIdentity.getPopulationId())) 155 { 156 _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true); 157 return userIdentity; 158 } 159 } 160 } 161 } 162 163 return null; 164 } 165 166 @Override 167 protected Optional<String> _getWeakPasswordURI(Request request, UserIdentity userIdentity) 168 { 169 Site site = (Site) request.getAttribute("site"); 170 171 String defaultLanguage = "en"; 172 Locale local = request.getLocale(); 173 174 String prefLanguage = local != null ? local.getLanguage() : defaultLanguage; 175 176 String weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage); 177 if (weakPasswordUrl == null) 178 { 179 // try to get weak password from local language 180 weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage); 181 } 182 183 if (weakPasswordUrl == null) 184 { 185 // try to get weak password from default language 186 weakPasswordUrl = site.getWeakPasswordUrl(defaultLanguage); 187 } 188 189 if (weakPasswordUrl == null) 190 { 191 for (String lang : site.getLanguages()) 192 { 193 weakPasswordUrl = site.getWeakPasswordUrl(lang); 194 if (weakPasswordUrl != null) 195 { 196 break; 197 } 198 } 199 } 200 201 return Optional.ofNullable(weakPasswordUrl + "&userIdentity=" + UserIdentity.userIdentityToString(userIdentity)); 202 } 203 204 /** 205 * Get the user identity of the connected user from the session 206 * @param request The request 207 * @return The connected useridentity or null 208 */ 209 public static UserIdentity getUserIdentityFromSession(Request request) 210 { 211 Site site = (Site) request.getAttribute("site"); 212 String siteName = site.getName(); 213 214 return getUserIdentityFromSession(request, siteName); 215 } 216 217 /** 218 * Get the user identity of the connected user from the session 219 * @param request The request 220 * @param siteName The current site name 221 * @return The connected useridentity or null 222 */ 223 public static UserIdentity getUserIdentityFromSession(Request request, String siteName) 224 { 225 Session session = request.getSession(false); 226 if (session != null) 227 { 228 return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY + "-" + siteName); 229 } 230 return null; 231 } 232 233 @Override 234 protected CredentialProvider _getCredentialProviderFromSession(Request request) 235 { 236 return getCredentialProviderFromSession(request); 237 } 238 239 /** 240 * Get the credential provider used for the current connection 241 * @param request The request 242 * @return The credential provider used or null 243 */ 244 public static CredentialProvider getCredentialProviderFromSession(Request request) 245 { 246 Site site = (Site) request.getAttribute("site"); 247 248 if (site == null) 249 { 250 return null; 251 } 252 253 String siteName = site.getName(); 254 255 return getCredentialProviderFromSession(request, siteName); 256 } 257 258 /** 259 * Get the credential provider used for the current connection 260 * @param request The request 261 * @param siteName The current site name 262 * @return The credential provider used or null 263 */ 264 public static CredentialProvider getCredentialProviderFromSession(Request request, String siteName) 265 { 266 Session session = request.getSession(false); 267 if (session != null) 268 { 269 return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName); 270 } 271 return null; 272 } 273 274 @Override 275 protected Boolean _getCredentialProviderModeFromSession(Request request) 276 { 277 return getCredentialProviderModeFromSession(request); 278 } 279 280 /** 281 * Get the credential provider mode used for the current connection 282 * @param request The request 283 * @return The credential provider mode used or null 284 */ 285 public static Boolean getCredentialProviderModeFromSession(Request request) 286 { 287 Site site = (Site) request.getAttribute("site"); 288 String siteName = site.getName(); 289 290 return getCredentialProviderModeFromSession(request, siteName); 291 } 292 293 /** 294 * Get the credential provider mode used for the current connection 295 * @param request The request 296 * @param siteName The current site name 297 * @return The credential provider mode used or null 298 */ 299 public static Boolean getCredentialProviderModeFromSession(Request request, String siteName) 300 { 301 Session session = request.getSession(false); 302 if (session != null) 303 { 304 return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName); 305 } 306 return null; 307 } 308 309 @Override 310 protected List<String> _getContexts(Request request, Parameters parameters) 311 { 312 Site site = (Site) request.getAttribute("site"); 313 String siteName = site.getName(); 314 return Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName); 315 } 316 317 @Override 318 protected String getLoginURL(Request request) 319 { 320 Site site = (Site) request.getAttribute("site"); 321 String siteName = site.getName(); 322 323 return getLoginURLParameters(request, "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/login/" + siteName); 324 } 325 326 @Override 327 protected String getLogoutURL(Request request) 328 { 329 Site site = (Site) request.getAttribute("site"); 330 String siteName = site.getName(); 331 return "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/logout/" + siteName; 332 } 333 334 @Override 335 protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception 336 { 337 boolean logout = super._handleLogout(redirector, objectModel, source, parameters); 338 if (logout) 339 { 340 // Additionally we need to destroy potential server side session 341 Request request = ObjectModelHelper.getRequest(objectModel); 342 HttpCookie sessionId = (HttpCookie) request.getCookieMap().get(GeneratePageAction.__BACKOFFICE_JSESSION_ID); 343 if (sessionId != null) 344 { 345 // FIXME CMS-12715 Reuse the same client for all request from site to cms 346 try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient()) 347 { 348 String cmsURL = Config.getInstance().getValue("org.ametys.site.bo"); 349 HttpGet httpGet = new HttpGet(cmsURL + "/logout.html"); 350 httpGet.addHeader("Cookie", "JSESSIONID=" + sessionId.getValue()); 351 int status = httpClient.execute(httpGet, HttpResponse::getCode); 352 if (status != 200) 353 { 354 getLogger().warn("BO responded to request to logout with status :" + status); 355 } 356 } 357 } 358 } 359 return logout; 360 } 361 362 @Override 363 protected UserIdentity _validateToken(String token, String context) 364 { 365 String tokenContext = context; 366 if (StringUtils.isBlank(tokenContext) 367 || Strings.CS.equals(tokenContext, "application")) 368 { 369 tokenContext = "sites"; 370 } 371 372 // Get site names and URLs from the CMS 373 String cmsURL = Config.getInstance().getValue("org.ametys.site.bo"); 374 375 // FIXME CMS-12715 Reuse the same client for all request from site to cms 376 try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient()) 377 { 378 HttpPost httpPost = new HttpPost(cmsURL + "/_validate_token.xml"); 379 httpPost.addHeader("X-Ametys-FO", "true"); 380 381 List<NameValuePair> nvps = new ArrayList<>(); 382 nvps.add(new BasicNameValuePair("token", token)); 383 nvps.add(new BasicNameValuePair("tokenContext", tokenContext)); 384 httpPost.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8)); 385 386 return httpClient.execute(httpPost, response -> { 387 388 switch (response.getCode()) 389 { 390 case 200: 391 break; 392 393 case 403: 394 throw new IllegalStateException("The CMS back-office refused the connection"); 395 396 case 500: 397 default: 398 throw new IllegalStateException("The CMS back-office returned an error"); 399 } 400 401 try (HttpEntity entity = response.getEntity(); 402 InputStream is = entity.getContent()) 403 { 404 Configuration conf = new DefaultConfigurationBuilder().build(is); 405 406 String login = conf.getChild("login").getValue(null); 407 String populationId = conf.getChild("populationId").getValue(null); 408 409 if (StringUtils.isNoneBlank(login, populationId)) 410 { 411 return new UserIdentity(login, populationId); 412 } 413 else 414 { 415 return null; 416 } 417 } 418 catch (ConfigurationException | SAXException e) 419 { 420 throw new LambdaException(e); 421 } 422 }); 423 } 424 catch (Exception e) 425 { 426 if (e instanceof LambdaException lambda) 427 { 428 e = (Exception) lambda.getCause(); // we can cast because we know that we only wrapped exception 429 } 430 throw new RuntimeException("Unable to synchronize site data", e); 431 } 432 } 433}