001/* 002 * Copyright 2017 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.core.authentication; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Enumeration; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.Optional; 029import java.util.Set; 030import java.util.regex.Pattern; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.activity.Initializable; 034import org.apache.avalon.framework.parameters.Parameters; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.thread.ThreadSafe; 037import org.apache.cocoon.ProcessingException; 038import org.apache.cocoon.acting.ServiceableAction; 039import org.apache.cocoon.environment.ObjectModelHelper; 040import org.apache.cocoon.environment.Redirector; 041import org.apache.cocoon.environment.Request; 042import org.apache.cocoon.environment.Session; 043import org.apache.cocoon.environment.SourceResolver; 044import org.apache.commons.lang3.StringUtils; 045 046import org.ametys.core.ObservationConstants; 047import org.ametys.core.authentication.token.AuthenticationTokenManager; 048import org.ametys.core.observation.Event; 049import org.ametys.core.observation.ObservationManager; 050import org.ametys.core.user.CurrentUserProvider; 051import org.ametys.core.user.User; 052import org.ametys.core.user.UserIdentity; 053import org.ametys.core.user.UserManager; 054import org.ametys.core.user.population.PopulationContextHelper; 055import org.ametys.core.user.population.UserPopulation; 056import org.ametys.core.user.population.UserPopulationDAO; 057import org.ametys.core.util.URIUtils; 058import org.ametys.plugins.core.impl.authentication.FormCredentialProvider; 059import org.ametys.plugins.core.user.UserDAO; 060import org.ametys.runtime.authentication.AccessDeniedException; 061import org.ametys.runtime.authentication.AuthorizationRequiredException; 062import org.ametys.runtime.workspace.WorkspaceMatcher; 063 064/** 065 * Cocoon action to perform authentication.<br> 066 * The {@link CredentialProvider} define the authentication method and retrieves {@link Credentials}.<br> 067 * Finally, the Users instance extract the Principal corresponding to the {@link Credentials}. 068 */ 069public class AuthenticateAction extends ServiceableAction implements ThreadSafe, Initializable 070{ 071 /** The request attribute to allow internal action from an internal request. */ 072 public static final String REQUEST_ATTRIBUTE_INTERNAL_ALLOWED = "Runtime:InternalAllowedRequest"; 073 074 /** The request attribute meaning that the request was not authenticated but granted */ 075 public static final String REQUEST_ATTRIBUTE_GRANTED = "Runtime:GrantedRequest"; 076 /** The request attribute name for transmitting the list of user populations */ 077 public static final String REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST = "Runtime:UserPopulationsList"; 078 /** The request attribute name for transmitting the currently chosen user population */ 079 public static final String REQUEST_ATTRIBUTE_USER_POPULATION_ID = "Runtime:CurrentUserPopulationId"; 080 /** The request attribute name for transmitting the login page url */ 081 public static final String REQUEST_ATTRIBUTE_LOGIN_URL = "Runtime:RequestLoginURL"; 082 083 /** Name of the user population HTML field */ 084 public static final String REQUEST_PARAMETER_POPULATION_NAME = "UserPopulation"; 085 /** Name of the credential provider index HTML field */ 086 public static final String REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX = "CredentialProviderIndex"; 087 088 /** The request attribute name for indicating that the authentication process has been made. */ 089 public static final String REQUEST_ATTRIBUTE_AUTHENTICATED = "Runtime:RequestAuthenticated"; 090 091 /** The request parameter holding the token */ 092 public static final String REQUEST_PARAMETER_TOKEN = "token"; 093 /** The request parameter holding the token context */ 094 public static final String REQUEST_PARAMETER_TOKEN_CONTEXT = "tokenContext"; 095 /** The header parameter that can be set to handle the token */ 096 public static final String HEADER_TOKEN = "X-Ametys-Token"; 097 098 /** The sitemap parameter holding the token */ 099 protected static final String PARAMETERS_PARAMETER_TOKEN = "token"; 100 /** The sitemap parameter holding the token context */ 101 protected static final String PARAMETERS_PARAMETER_TOKEN_CONTEXT = "tokenContext"; 102 /** The request attribute name for transmitting a boolean that tell if there is a list of credential provider to choose */ 103 protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST = "Runtime:RequestListCredentialProvider"; 104 /** The request attribute name for transmitting the index in the list of chosen credential provider */ 105 protected static final String REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX = "Runtime:RequestCredentialProviderIndex"; 106 /** The request attribute name to know if user population list should be proposed */ 107 protected static final String REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST = "Runtime:UserPopulationsListDisplay"; 108 /** The request attribute name for transmitting the potential list of user populations to the login screen . */ 109 protected static final String REQUEST_ATTRIBUTE_INVALID_POPULATION = "Runtime:RequestInvalidPopulation"; 110 /** The request attribute name for transmitting the list of contexts */ 111 protected static final String REQUEST_ATTRIBUTE_CONTEXTS = "Runtime:Contexts"; 112 113 /** The session attribute name for storing the credential provider index of the authentication (during connection process) */ 114 protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX = "Runtime:ConnectingCredentialProviderIndex"; 115 /** The session attribute name for storing the last known credential provider index of the authentication (during connection process)*/ 116 protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN = "Runtime:ConnectingCredentialProviderIndexLastKnown"; 117 /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=>false, blocking=>true (during connection process) */ 118 protected static final String SESSION_CONNECTING_CREDENTIALPROVIDER_MODE = "Runtime:ConnectingCredentialProviderMode"; 119 /** The session attribute name for storing the id of the user population (during connection process) */ 120 protected static final String SESSION_CONNECTING_USERPOPULATION_ID = "Runtime:ConnectingUserPopulationId"; 121 122 /** The session attribute name for storing the credential provider of the authentication */ 123 protected static final String SESSION_CREDENTIALPROVIDER = "Runtime:CredentialProvider"; 124 /** The session attribute name for storing the credential provider mode of the authentication: non-blocking=>false, blocking=>true */ 125 protected static final String SESSION_CREDENTIALPROVIDER_MODE = "Runtime:CredentialProviderMode"; 126 /** The session attribute name for storing the identity of the connected user */ 127 protected static final String SESSION_USERIDENTITY = "Runtime:UserIdentity"; 128 129 /** The sitemap parameter to set the token mode of the action */ 130 protected static final String SITEMAP_PARAMETER_TOKEN_MODE = "token-mode"; 131 132 /** The DAO for user populations */ 133 protected UserPopulationDAO _userPopulationDAO; 134 /** The user manager */ 135 protected UserManager _userManager; 136 /** The helper for the associations population/context */ 137 protected PopulationContextHelper _populationContextHelper; 138 /** The current user provider */ 139 protected CurrentUserProvider _currentUserProvider; 140 141 /** url requires for authentication */ 142 protected Collection<Pattern> _acceptedUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/core/authenticate/[0-9]+$")}); 143 144 /** The authentication token manager */ 145 protected AuthenticationTokenManager _authenticateTokenManager; 146 /** The observation manager */ 147 protected ObservationManager _observationManager; 148 149 /** 150 * The token mode of this authentication action 151 */ 152 protected enum TOKEN_MODE 153 { 154 /** In this mode, only the token will be taken in account. If no token is found, authentication will not be considered done */ 155 TOKEN_ONLY, 156 /** In this mode, the token will be taken in account but if no token is found, user will be considered as anonymous and authentication will be considered done */ 157 ALLOW_ANONYMOUS, 158 /** In this default mode, the token will be taken in account, but if no token is found, the authentication process will continue */ 159 DEFAULT 160 } 161 162 @Override 163 public void initialize() throws Exception 164 { 165 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 166 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 167 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 168 169 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 170 try 171 { 172 _authenticateTokenManager = (AuthenticationTokenManager) manager.lookup(AuthenticationTokenManager.ROLE); 173 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 174 } 175 catch (ServiceException e) 176 { 177 // nothing... we are in safe mode, but this is not safe 178 } 179 } 180 181 @Override 182 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 183 { 184 Request request = ObjectModelHelper.getRequest(objectModel); 185 186 if (_preFlightCheck(redirector, resolver, objectModel, source, parameters) || _handleAuthenticationToken(request, parameters)) 187 { 188 // We passed the authentication, let's mark it now 189 request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true"); 190 191 // We passed the authentication (with a user) 192 return EMPTY_MAP; 193 } 194 195 // At this point, the user is still anonymous 196 197 // If only token are authorized for authentication, stop authentication process. There is no user authenticated here. 198 if (_getTokenMode(parameters) != TOKEN_MODE.DEFAULT) 199 { 200 if (_getTokenMode(parameters) == TOKEN_MODE.ALLOW_ANONYMOUS) 201 { 202 request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true"); 203 } 204 return null; 205 } 206 207 // At this point, we already know that the entire process will be executed, whatever the outcome 208 // Set the flag, so that the authentication process won't repeat 209 request.setAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED, "true"); 210 211 // Get population and if possible credential providers 212 List<UserPopulation> chosenUserPopulations = new ArrayList<>(); 213 List<CredentialProvider> credentialProviders = new ArrayList<>(); 214 if (!_prepareUserPopulationsAndCredentialProviders(request, parameters, redirector, chosenUserPopulations, credentialProviders)) 215 { 216 // Let's display the population screen 217 return EMPTY_MAP; 218 } 219 220 // Get the currently running credential provider 221 int runningCredentialProviderIndex = _getCurrentCredentialProviderIndex(request, credentialProviders); 222 request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX, runningCredentialProviderIndex); 223 request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_URL, getLoginURL(request)); 224 225 // Let's process non-blocking 226 if (!_isCurrentCredentialProviderInBlockingMode(request)) 227 { 228 // if there was no one running, let's start with the first one 229 runningCredentialProviderIndex = Math.max(0, runningCredentialProviderIndex); 230 231 for (; runningCredentialProviderIndex < credentialProviders.size(); runningCredentialProviderIndex++) 232 { 233 CredentialProvider runningCredentialProvider = credentialProviders.get(runningCredentialProviderIndex); 234 if (_process(request, false, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations)) 235 { 236 // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment 237 return EMPTY_MAP; 238 } 239 } 240 241 // No one matches 242 runningCredentialProviderIndex = -1; 243 } 244 245 _saveLastKnownBlockingCredentialProvider(request, runningCredentialProviderIndex); 246 247 // Let's process the current blocking one or the only existing one 248 if (_shouldRunFirstBlockingCredentialProvider(runningCredentialProviderIndex, credentialProviders, request, chosenUserPopulations)) 249 { 250 CredentialProvider runningCredentialProvider = runningCredentialProviderIndex == -1 ? _getFirstBlockingCredentialProvider(credentialProviders) : credentialProviders.get(runningCredentialProviderIndex); 251 if (_process(request, true, runningCredentialProvider, runningCredentialProviderIndex, redirector, chosenUserPopulations)) 252 { 253 // Whatever the user was correctly authenticated or he just required a redirect: let's stop here for the moment 254 return EMPTY_MAP; 255 } 256 257 throw new AuthorizationRequiredException(); 258 } 259 260 // At this step we have two kind off requests 261 // 1) A secondary request of a blocking cp (such as captcha image...) 262 Integer formerRunningCredentialProviderIndex = (Integer) request.getSession(true).getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN); 263 if (formerRunningCredentialProviderIndex != null && credentialProviders.get(formerRunningCredentialProviderIndex).grantAnonymousRequest(true)) 264 { 265 // Anonymous request 266 request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true); 267 _saveConnectingStateToSession(request, -1, true); 268 return EMPTY_MAP; 269 } 270 271 // 2) Or a main stream request that should display the list of available blocking cp 272 return _displayBlockingList(redirector, request, credentialProviders); 273 } 274 275 /** 276 * Prepare authentication 277 * @param redirector The redirector 278 * @param resolver The source resolver 279 * @param objectModel The object model 280 * @param source The source 281 * @param parameters The action parameters 282 * @return <code>true</code> if a user was authenticated, <code>false</code> otherwise 283 * @throws Exception if failed to prepare the authentication 284 */ 285 protected boolean _preFlightCheck(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 286 { 287 Request request = ObjectModelHelper.getRequest(objectModel); 288 289 return _handleLogout(redirector, objectModel, source, parameters) // Test if user wants to logout 290 || _internalRequest(request) // Test if this request was already authenticated or it the request is marked as an internal one 291 || _acceptedUrl(request) // Test if the url is used for authentication 292 || _validateCurrentlyConnectedUser(request, redirector, parameters) // Test if the currently connected user is still valid 293 || redirector.hasRedirected(); 294 } 295 296 /** 297 * Authenticate a user using the token in request (if configured so) 298 * @param request The request 299 * @param parameters The action parameters 300 * @return true if the user was authenticated 301 */ 302 protected boolean _handleAuthenticationToken(Request request, Parameters parameters) 303 { 304 String token = request.getHeader(HEADER_TOKEN); 305 if (StringUtils.isBlank(token)) 306 { 307 token = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN, _getTokenFromRequest(request)); 308 } 309 310 if (StringUtils.isNotBlank(token)) 311 { 312 String context = parameters.getParameter(PARAMETERS_PARAMETER_TOKEN_CONTEXT, null); 313 UserIdentity userIdentity = _validateToken(token, context); 314 if (userIdentity != null) 315 { 316 // Save user identity 317 _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true); 318 _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userIdentity, request, parameters); 319 return true; 320 } 321 } 322 323 return false; 324 } 325 326 /** 327 * Get the token from the request 328 * @param request The request 329 * @return The token from the request or null 330 */ 331 protected String _getTokenFromRequest(Request request) 332 { 333 // FIXME RUNTIME-2501 check the parameter is provided in POST, e.g. by seeking if '?token=' and '&token=' are not used in request uri... 334 return request.getParameter(REQUEST_PARAMETER_TOKEN); 335 } 336 337 /** 338 * Validate the given token 339 * @param token The non empty token to validate 340 * @param context the context on which the token should be validated 341 * @return The corresponding user identity or null 342 */ 343 protected UserIdentity _validateToken(String token, String context) 344 { 345 return _authenticateTokenManager != null ? _authenticateTokenManager.validateToken(token, context) : null; 346 } 347 348 private TOKEN_MODE _getTokenMode(Parameters parameters) 349 { 350 return TOKEN_MODE.valueOf(parameters.getParameter(SITEMAP_PARAMETER_TOKEN_MODE, TOKEN_MODE.DEFAULT.toString()).toUpperCase()); 351 } 352 353 private void _saveLastKnownBlockingCredentialProvider(Request request, int runningCredentialProviderIndex) 354 { 355 if (runningCredentialProviderIndex != -1) 356 { 357 request.getSession(true).setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN, runningCredentialProviderIndex); 358 } 359 } 360 361 private Map _displayBlockingList(Redirector redirector, Request request, List<CredentialProvider> credentialProviders) throws IOException, ProcessingException, AuthorizationRequiredException 362 { 363 if (credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst().isPresent()) 364 { 365 _saveConnectingStateToSession(request, -1, true); 366 redirector.redirect(false, getLoginURL(request)); 367 return EMPTY_MAP; 368 } 369 else 370 { 371 // No way to login 372 throw new AuthorizationRequiredException(); 373 } 374 } 375 376 @SuppressWarnings("unchecked") 377 private boolean _shouldRunFirstBlockingCredentialProvider(int runningCredentialProviderIndex, List<CredentialProvider> credentialProviders, Request request, List<UserPopulation> chosenUserPopulations) 378 { 379 return runningCredentialProviderIndex >= 0 // There is a running credential provider 380 || credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).count() == 1 // There is a single blocking credential provider AND 381 && ( 382 ((List<UserPopulation>) request.getAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST)).size() == chosenUserPopulations.size() // no population choice screen 383 || _getFirstBlockingCredentialProvider(credentialProviders).requiresNewWindow() // it does not requires a window opening 384 ); 385 } 386 387 private BlockingCredentialProvider _getFirstBlockingCredentialProvider(List<CredentialProvider> credentialProviders) 388 { 389 Optional<CredentialProvider> findFirst = credentialProviders.stream().filter(cp -> cp instanceof BlockingCredentialProvider).findFirst(); 390 if (findFirst.isPresent()) 391 { 392 return (BlockingCredentialProvider) findFirst.get(); 393 } 394 else 395 { 396 return null; 397 } 398 } 399 400 /** 401 * Fill the list of available users populations and credential providers 402 * @param request The request 403 * @param parameters The action parameters 404 * @param redirector The cocoon redirector 405 * @param chosenUserPopulations An empty non-null list to fill with with chosen populations 406 * @param credentialProviders An empty non-null list to fill with chosen credential providers 407 * @return true, if the population was determined, false if a redirection was required to choose 408 * @throws IOException If an error occurred 409 * @throws ProcessingException If an error occurred 410 */ 411 protected boolean _prepareUserPopulationsAndCredentialProviders(Request request, Parameters parameters, Redirector redirector, List<UserPopulation> chosenUserPopulations, List<CredentialProvider> credentialProviders) throws ProcessingException, IOException 412 { 413 // Get contexts 414 List<String> contexts = _getContexts(request, parameters); 415 request.setAttribute(REQUEST_ATTRIBUTE_CONTEXTS, contexts); 416 417 // All user populations for this context 418 List<UserPopulation> availableUserPopulations = _getAvailableUserPopulationsIds(request, contexts).stream().map(_userPopulationDAO::getUserPopulation).collect(Collectors.toList()); 419 request.setAttribute(REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST, availableUserPopulations); 420 421 // Chosen population 422 String userPopulationId = _getChosenUserPopulationId(request, availableUserPopulations); 423 request.setAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID, userPopulationId); 424 425 chosenUserPopulations.addAll(userPopulationId == null ? availableUserPopulations : Collections.singletonList(_userPopulationDAO.getUserPopulation(userPopulationId))); 426 if (chosenUserPopulations.size() == 0) 427 { 428 String redirection = parameters.getParameter("nocontext-redirection", null); 429 if (redirection == null) 430 { 431 throw new IllegalStateException("There is no populations available for contexts '" + StringUtils.join(contexts, "', '") + "'"); 432 } 433 else 434 { 435 redirector.redirect(false, redirection); 436 return false; 437 } 438 } 439 440 // Get possible credential providers 441 boolean availableCredentialProviders = _hasCredentialProviders(chosenUserPopulations); 442 request.setAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST, availableCredentialProviders); 443 444 // null means the credential providers cannot be determine without knowing population first 445 if (!availableCredentialProviders) 446 { 447 request.setAttribute(REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST, true); 448 449 // if we are on this screen after a 'back' button hit, we need to reset connecting information 450 _resetConnectingStateToSession(request); 451 452 // Screen "Where Are You From?" with the list of populations to select 453 if (redirector != null) 454 { 455 redirector.redirect(false, getLoginURL(request)); 456 } 457 return false; 458 } 459 else 460 { 461 credentialProviders.addAll(chosenUserPopulations.get(0).getCredentialProviders()); 462 if (credentialProviders.size() == 0) 463 { 464 throw new IllegalStateException("There is no populations credential provider available for contexts '" + StringUtils.join(contexts, "', '") + "'"); 465 } 466 request.setAttribute(REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST, userPopulationId == null || _hasCredentialProviders(availableUserPopulations) || credentialProviders.size() == 1 && !credentialProviders.stream().filter(cp -> cp instanceof FormCredentialProvider).findAny().isPresent()); 467 return true; 468 } 469 } 470 471 /** 472 * Get the url for the redirector to display the login screen 473 * @param request The request 474 * @return The url. Cannot be null or empty 475 */ 476 protected String getLoginURL(Request request) 477 { 478 return getLoginURLParameters(request, "cocoon://_plugins/core/login.html"); 479 } 480 481 482 /** 483 * Get the url for the redirector to display the login screen 484 * @param request The request 485 * @param baseURL The url to complete with parameters 486 * @return The url. Cannot be null or empty 487 */ 488 @SuppressWarnings("unchecked") 489 protected String getLoginURLParameters(Request request, String baseURL) 490 { 491 List<String> parameters = new ArrayList<>(); 492 493 Boolean invalidPopulationIds = (Boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INVALID_POPULATION); 494 parameters.add("invalidPopulationIds=" + (invalidPopulationIds == Boolean.TRUE ? "true" : "false")); 495 496 boolean shouldDisplayUserPopulationsList = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_SHOULD_DISPLAY_USER_POPULATIONS_LIST); 497 parameters.add("shouldDisplayUserPopulationsList=" + (shouldDisplayUserPopulationsList ? "true" : "false")); 498 499 List<UserPopulation> usersPopulations = (List<UserPopulation>) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST); 500 if (usersPopulations != null) 501 { 502 parameters.add("usersPopulations=" + URIUtils.encodeParameter(StringUtils.join(usersPopulations.stream().map(UserPopulation::getId).collect(Collectors.toList()), ","))); 503 } 504 505 String chosenPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID); 506 if (chosenPopulationId != null) 507 { 508 parameters.add("chosenPopulationId=" + URIUtils.encodeParameter(chosenPopulationId)); 509 } 510 511 boolean availableCredentialProviders = (boolean) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_LIST); 512 parameters.add("availableCredentialProviders=" + (availableCredentialProviders ? "true" : "false")); 513 514 Integer credentialProviderIndex = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_CREDENTIAL_PROVIDER_INDEX); 515 parameters.add("credentialProviderIndex=" + String.valueOf(credentialProviderIndex != null ? credentialProviderIndex : -1)); 516 517 List<String> contexts = (List<String>) request.getAttribute(REQUEST_ATTRIBUTE_CONTEXTS); 518 parameters.add("contexts=" + URIUtils.encodeParameter(StringUtils.join(contexts, ","))); 519 520 return baseURL + (baseURL.contains("?") ? "&" : "?") + StringUtils.join(parameters, "&"); 521 } 522 523 /** 524 * Get the url for the redirector to display the logout screen 525 * @param request The request 526 * @return The url. Cannot be null or empty 527 */ 528 protected String getLogoutURL(Request request) 529 { 530 return "cocoon://_plugins/core/logout.html"; 531 } 532 533 /** 534 * Determine if there is a list of credential providers to use 535 * @param userPopulations The list of applicable user populations 536 * @return true if credentialproviders can be used 537 */ 538 protected boolean _hasCredentialProviders(List<UserPopulation> userPopulations) 539 { 540 // Is there only one population or all populations have the same credential provider list as the first one? 541 if (userPopulations.size() == 1 || userPopulations.stream().map(UserPopulation::getCredentialProviders).distinct().count() == 1) 542 { 543 return true; 544 } 545 546 // Cannot determine the list 547 return false; 548 } 549 550 /** 551 * Get the available populations for the given contexts 552 * @param request The request 553 * @param contexts The contexts 554 * @return The non-null list of populations 555 */ 556 protected Set<String> _getAvailableUserPopulationsIds(Request request, List<String> contexts) 557 { 558 return _populationContextHelper.getUserPopulationsOnContexts(contexts, false, false); 559 } 560 561 /** 562 * Get the population for the given context 563 * @param request The request 564 * @param availableUserPopulations The available users populations 565 * @return The chosen population id. Can be null. 566 */ 567 protected String _getChosenUserPopulationId(Request request, List<UserPopulation> availableUserPopulations) 568 { 569 // Get request population choice 570 String userPopulationId = request.getParameter(REQUEST_PARAMETER_POPULATION_NAME); 571 if (userPopulationId == null) 572 { 573 // Get memorized population choice 574 Session session = request.getSession(false); 575 if (session != null) 576 { 577 userPopulationId = (String) session.getAttribute(SESSION_CONNECTING_USERPOPULATION_ID); 578 session.setAttribute(SESSION_CONNECTING_USERPOPULATION_ID, null); 579 } 580 } 581 582 // A population choice was already made 583 if (StringUtils.isNotBlank(userPopulationId)) 584 { 585 final String finalUserPopulationId = userPopulationId; 586 if (availableUserPopulations.stream().anyMatch(userPopulation -> userPopulation.getId().equals(finalUserPopulationId))) 587 { 588 return userPopulationId; 589 } 590 else 591 { 592 // Wrong submitted population id 593 request.setAttribute(REQUEST_ATTRIBUTE_INVALID_POPULATION, true); 594 } 595 } 596 597 return null; 598 } 599 600 /** 601 * Try to authenticate with this credential provider in this mode. Delegates to _doProcess 602 * @param request The request 603 * @param runningBlockingkMode false for non-blocking mode, true for blocking mode 604 * @param runningCredentialProvider the Credential provider to test 605 * @param runningCredentialProviderIndex The index of the currently tested credential provider 606 * @param redirector The cocoon redirector 607 * @param userPopulations The list of possible user populations 608 * @return false if we should try with another Credential provider, true otherwise 609 * @throws Exception If an error occurred 610 */ 611 protected boolean _process(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, int runningCredentialProviderIndex, Redirector redirector, List<UserPopulation> userPopulations) throws Exception 612 { 613 boolean existingSession = request.getSession(false) != null; 614 _saveConnectingStateToSession(request, runningBlockingkMode ? -1 : runningCredentialProviderIndex, runningBlockingkMode); 615 if (_doProcess(request, runningBlockingkMode, runningCredentialProvider, redirector, userPopulations)) 616 { 617 return true; 618 } 619 if (existingSession) 620 { 621 // A session was created but finally we do not need it 622 request.getSession().invalidate(); 623 } 624 return false; 625 } 626 627 /** 628 * Try to authenticate with this credential provider in this mode 629 * @param request The request 630 * @param runningBlockingkMode false for non-blocking mode, true for blocking mode 631 * @param runningCredentialProvider the Credential provider to test 632 * @param redirector The cocoon redirector 633 * @param userPopulations The list of possible user populations 634 * @return false if we should try with another Credential provider, true otherwise 635 * @throws Exception If an error occurred 636 */ 637 638 protected boolean _doProcess(Request request, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider, Redirector redirector, List<UserPopulation> userPopulations) throws Exception 639 { 640 if (runningCredentialProvider.grantAnonymousRequest(runningBlockingkMode)) 641 { 642 // Anonymous request 643 request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true); 644 return true; 645 } 646 647 UserIdentity potentialUserIdentity = runningCredentialProvider.getUserIdentity(runningBlockingkMode, redirector); 648 if (redirector.hasRedirected()) 649 { 650 // getCredentials require a redirection, save state and proceed 651 return true; 652 } 653 else if (potentialUserIdentity == null) 654 { 655 // Let us try another credential provider 656 return false; 657 } 658 659 // Check if user exists 660 UserIdentity userIdentity = _getUserIdentity(userPopulations, potentialUserIdentity, redirector, runningBlockingkMode, runningCredentialProvider); 661 if (redirector.hasRedirected()) 662 { 663 // getCredentials require a redirection, save state and proceed 664 return true; 665 } 666 else if (userIdentity == null) 667 { 668 // Let us try another credential provider 669 return false; 670 } 671 672 // Save user identity 673 _setUserIdentityInSession(request, userIdentity, runningCredentialProvider, runningBlockingkMode); 674 675 // Authentication succeeded 676 runningCredentialProvider.userAllowed(runningBlockingkMode, userIdentity, redirector); 677 678 return true; 679 } 680 681 /** 682 * Reset the connecting information in session 683 * @param request The request 684 */ 685 protected static void _resetConnectingStateToSession(Request request) 686 { 687 Session session = request.getSession(false); 688 if (session != null) 689 { 690 session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX); 691 session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE); 692 session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX_LASTBLOCKINGKNOWN); 693 session.removeAttribute(SESSION_CONNECTING_USERPOPULATION_ID); 694 } 695 } 696 697 /** 698 * When the process end successfully, save the state 699 * @param request The request 700 * @param runningBlockingkMode false for non-blocking mode, true for blocking mode 701 * @param runningCredentialProviderIndex the currently tested credential provider 702 */ 703 protected void _saveConnectingStateToSession(Request request, int runningCredentialProviderIndex, boolean runningBlockingkMode) 704 { 705 Session session = request.getSession(true); 706 session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, runningCredentialProviderIndex); 707 session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE, runningBlockingkMode); 708 session.setAttribute(SESSION_CONNECTING_USERPOPULATION_ID, request.getAttribute(REQUEST_ATTRIBUTE_USER_POPULATION_ID)); 709 } 710 711 /** 712 * Save user identity in request 713 * @param request The request 714 * @param userIdentity The useridentity to save 715 * @param credentialProvider The credential provider used to connect 716 * @param blockingMode The mode used for the credential provider 717 */ 718 protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode) 719 { 720 setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode); 721 if (_observationManager != null) 722 { 723 Map<String, Object> eventParams = new HashMap<>(); 724 eventParams.put(ObservationConstants.ARGS_USER, userIdentity); 725 _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams)); 726 } 727 } 728 729 /** 730 * Save user identity in request 731 * @param request The request 732 * @param userIdentity The useridentity to save 733 * @param credentialProvider The credential provider used to connect 734 * @param blockingMode The mode used for the credential provider 735 */ 736 public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode) 737 { 738 Session session = _renewSession(request); 739 _resetConnectingStateToSession(request); 740 session.setAttribute(SESSION_USERIDENTITY, userIdentity); 741 session.setAttribute(SESSION_CREDENTIALPROVIDER, credentialProvider); 742 session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE, blockingMode); 743 } 744 745 /** 746 * Change the session id (for security purposes) 747 * @param request The current request 748 * @return The new session 749 */ 750 protected static Session _renewSession(Request request) 751 { 752 Session session = request.getSession(true); 753 754 Map<String, Object> attributesCopy = new HashMap<>(); 755 756 Enumeration<String> attributeNames = session.getAttributeNames(); 757 while (attributeNames.hasMoreElements()) 758 { 759 String attributeName = attributeNames.nextElement(); 760 attributesCopy.put(attributeName, session.getAttribute(attributeName)); 761 } 762 763 session.invalidate(); 764 765 session = request.getSession(true); 766 767 for (Entry<String, Object> attribute : attributesCopy.entrySet()) 768 { 769 session.setAttribute(attribute.getKey(), attribute.getValue()); 770 } 771 772 return session; 773 } 774 775 /** 776 * Get the user identity of the connected user from the session 777 * @param request The request 778 * @return The connected useridentity or null 779 */ 780 protected UserIdentity _getUserIdentityFromSession(Request request) 781 { 782 return getUserIdentityFromSession(request); 783 } 784 785 /** 786 * Get the user identity of the connected user from the session 787 * @param request The request 788 * @return The connected useridentity or null 789 */ 790 public static UserIdentity getUserIdentityFromSession(Request request) 791 { 792 Session session = request.getSession(false); 793 if (session != null) 794 { 795 return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY); 796 } 797 return null; 798 } 799 800 /** 801 * Get the credential provider used for the current connection 802 * @param request The request 803 * @return The credential provider used or null 804 */ 805 protected CredentialProvider _getCredentialProviderFromSession(Request request) 806 { 807 return getCredentialProviderFromSession(request); 808 } 809 810 /** 811 * Get the credential provider used for the current connection 812 * @param request The request 813 * @return The credential provider used or null 814 */ 815 public static CredentialProvider getCredentialProviderFromSession(Request request) 816 { 817 Session session = request.getSession(false); 818 if (session != null) 819 { 820 return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER); 821 } 822 return null; 823 } 824 825 /** 826 * Get the credential provider mode used for the current connection 827 * @param request The request 828 * @return The credential provider mode used or null 829 */ 830 protected Boolean _getCredentialProviderModeFromSession(Request request) 831 { 832 return getCredentialProviderModeFromSession(request); 833 } 834 835 /** 836 * Get the credential provider mode used for the current connection 837 * @param request The request 838 * @return The credential provider mode used or null 839 */ 840 public static Boolean getCredentialProviderModeFromSession(Request request) 841 { 842 Session session = request.getSession(false); 843 if (session != null) 844 { 845 return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE); 846 } 847 return null; 848 } 849 850 /** 851 * If there is a running credential provider, was it in non-blocking or blocking mode? 852 * @param request The request 853 * @return false if non-blocking, true if blocking 854 */ 855 protected boolean _isCurrentCredentialProviderInBlockingMode(Request request) 856 { 857 Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request); 858 if (requestedCredentialParameterIndex != null && requestedCredentialParameterIndex != -1) 859 { 860 return true; 861 } 862 863 Session session = request.getSession(false); 864 if (session != null) 865 { 866 Boolean mode = (Boolean) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE); 867 session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_MODE); 868 if (mode != null) 869 { 870 return mode.booleanValue(); 871 } 872 } 873 return false; 874 } 875 876 /** 877 * Call this to skip the currently used credential provider and proceed to the next one. 878 * Useful for non blocking 879 * @param request The request 880 */ 881 public static void skipCurrentCredentialProvider(Request request) 882 { 883 Session session = request.getSession(); 884 if (session != null) 885 { 886 Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX); 887 if (cpIndex != null) 888 { 889 cpIndex++; 890 session.setAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX, cpIndex); 891 } 892 } 893 } 894 895 /** 896 * Get the current credential provider index or -1 if there no running provider FROM REQUEST PARAMETER 897 * @param request The request 898 * @return The credential provider index to use in the availablesCredentialProviders list or -1 or null 899 */ 900 protected Integer _getCurrentCredentialProviderIndexFromParameter(Request request) 901 { 902 // Is the CP requested? 903 String requestedCredentialParameterIndex = request.getParameter(REQUEST_PARAMETER_CREDENTIALPROVIDER_INDEX); 904 if (StringUtils.isNotBlank(requestedCredentialParameterIndex)) 905 { 906 int index = Integer.parseInt(requestedCredentialParameterIndex); 907 return index; 908 } 909 return null; 910 } 911 912 /** 913 * Get the current credential provider index or -1 if there no running provider 914 * @param request The request 915 * @param availableCredentialProviders The list of available credential provider 916 * @return The credential provider index to use in the availablesCredentialProviders list or -1 917 */ 918 protected int _getCurrentCredentialProviderIndex(Request request, List<CredentialProvider> availableCredentialProviders) 919 { 920 // Is the CP requested? 921 Integer requestedCredentialParameterIndex = _getCurrentCredentialProviderIndexFromParameter(request); 922 if (requestedCredentialParameterIndex != null) 923 { 924 if (requestedCredentialParameterIndex < availableCredentialProviders.size()) 925 { 926 return requestedCredentialParameterIndex; 927 } 928 else 929 { 930 return -1; 931 } 932 } 933 934 // Was the CP memorized? 935 Session session = request.getSession(false); 936 if (session != null) 937 { 938 Integer cpIndex = (Integer) session.getAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX); 939 session.removeAttribute(SESSION_CONNECTING_CREDENTIALPROVIDER_INDEX); 940 941 if (cpIndex != null) 942 { 943 return cpIndex; 944 } 945 } 946 947 // Default value 948 return -1; 949 } 950 951 /** 952 * Get the authentication context 953 * @param request The request 954 * @param parameters The action parameters 955 * @return The context 956 * @throws IllegalArgumentException If there is no context set 957 */ 958 protected List<String> _getContexts(Request request, Parameters parameters) 959 { 960 String context = parameters.getParameter("context", null); 961 if (context == null) 962 { 963 throw new IllegalArgumentException("The authentication is not parameterized correctly: an authentication context must be specified"); 964 } 965 return Collections.singletonList(context); 966 } 967 968 /** 969 * Determine if the request is internal and do not need authentication 970 * @param request The request 971 * @return true to bypass this authentication 972 */ 973 protected boolean _internalRequest(Request request) 974 { 975 return "true".equals(request.getAttribute(REQUEST_ATTRIBUTE_AUTHENTICATED)) || request.getAttribute(REQUEST_ATTRIBUTE_INTERNAL_ALLOWED) != null; 976 } 977 978 /** 979 * Determine if the request is one of the authentication process (except the credential providers) 980 * @param request The request 981 * @return true to bypass this authentication 982 */ 983 protected boolean _acceptedUrl(Request request) 984 { 985 // URL without server context and leading slash. 986 String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL); 987 for (Pattern pattern : _acceptedUrlPatterns) 988 { 989 if (pattern.matcher(url).matches()) 990 { 991 // Anonymous request 992 request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true); 993 994 return true; 995 } 996 } 997 998 return false; 999 } 1000 1001 /** 1002 * This method ensure that there is a currently connected user and that it is still valid 1003 * @param request The request 1004 * @param redirector The cocoon redirector 1005 * @param parameters The action parameters 1006 * @return true if the user is connected and valid 1007 * @throws Exception if an error occurred 1008 */ 1009 protected boolean _validateCurrentlyConnectedUser(Request request, Redirector redirector, Parameters parameters) throws Exception 1010 { 1011 Session session = request.getSession(false); 1012 UserIdentity userCurrentlyConnected = _getUserIdentityFromSession(request); 1013 CredentialProvider runningCredentialProvider = _getCredentialProviderFromSession(request); 1014 Boolean runningBlockingkMode = _getCredentialProviderModeFromSession(request); 1015 1016 if (runningCredentialProvider == null || userCurrentlyConnected == null || runningBlockingkMode == null || !runningCredentialProvider.isStillConnected(runningBlockingkMode, userCurrentlyConnected, redirector)) 1017 { 1018 if (redirector.hasRedirected()) 1019 { 1020 return true; 1021 } 1022 1023 // There is an invalid connected user 1024 if (session != null && userCurrentlyConnected != null) 1025 { 1026 session.invalidate(); 1027 } 1028 return false; 1029 } 1030 1031 // let us make an exception for the user image url since we need it on the 403 page 1032 if ("plugins/core-ui/current-user/image_64".equals(request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL))) 1033 { 1034 return true; 1035 } 1036 1037 _validateCurrentlyConnectedUserIsInAuthorizedPopulation(userCurrentlyConnected, request, parameters); 1038 1039 return true; 1040 } 1041 1042 /** 1043 * This method is the second part of the process that ensure that there is a currently connected user and that it is still valid 1044 * @param userCurrentlyConnected The user to test 1045 * @param request The request 1046 * @param parameters The action parameters 1047 */ 1048 protected void _validateCurrentlyConnectedUserIsInAuthorizedPopulation(UserIdentity userCurrentlyConnected, Request request, Parameters parameters) 1049 { 1050 if (_getTokenMode(parameters) == TOKEN_MODE.DEFAULT) 1051 { 1052 // we know this is a valid user, but we need to check if the context is correct 1053 List<String> contexts = _getContexts(request, parameters); 1054 // All user populations for this context 1055 Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, contexts); 1056 1057 if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId())) 1058 { 1059 throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated to the contexts '" + StringUtils.join(contexts, "', '") + "' because its populations are not part of the " + availableUserPopulationsIds.size() + " granted populations."); 1060 } 1061 } 1062 else 1063 { 1064 // In 'token only' mode, check if user is part of the active populations (regardless of the context) 1065 List<String> availableUserPopulationsIds = _userPopulationDAO.getEnabledUserPopulations(false).stream().map(UserPopulation::getId).collect(Collectors.toList()); 1066 1067 if (!availableUserPopulationsIds.contains(userCurrentlyConnected.getPopulationId())) 1068 { 1069 throw new AccessDeniedException("The user " + userCurrentlyConnected + " cannot be authenticated because its populations does not exist or it is disabled."); 1070 } 1071 } 1072 } 1073 1074 /** 1075 * Test if user wants to logout and handle it 1076 * @param redirector The cocoon redirector 1077 * @param objectModel The cocoon object model 1078 * @param source The sitemap source 1079 * @param parameters The sitemap parameters 1080 * @return true if the user was logged out 1081 * @throws Exception if an error occurred 1082 */ 1083 protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception 1084 { 1085 Request request = ObjectModelHelper.getRequest(objectModel); 1086 if (StringUtils.equals(request.getContextPath() + request.getAttribute(WorkspaceMatcher.WORKSPACE_URI) + "/logout.html", request.getRequestURI()) 1087 || StringUtils.equals("true", parameters.getParameter("logout", "false"))) 1088 { 1089 // The user logs out 1090 _currentUserProvider.logout(); 1091 if (!redirector.hasRedirected()) 1092 { 1093 redirector.redirect(false, getLogoutURL(request)); 1094 } 1095 return true; 1096 } 1097 return false; 1098 } 1099 1100 /** 1101 * Check the authentications of the authentication manager 1102 * @param userPopulations The list of available matching populations 1103 * @param redirector The cocoon redirector 1104 * @param runningBlockingkMode false for non-blocking mode, true for blocking mode 1105 * @param runningCredentialProvider The Credential provider to test 1106 * @param potentialUserIdentity A possible user identity. Population can be null. User may not exist either. 1107 * @return The user population matching credentials or null 1108 * @throws Exception If an error occurred 1109 * @throws AccessDeniedException If the user is rejected 1110 */ 1111 protected UserIdentity _getUserIdentity(List<UserPopulation> userPopulations, UserIdentity potentialUserIdentity, Redirector redirector, boolean runningBlockingkMode, CredentialProvider runningCredentialProvider) throws Exception 1112 { 1113 if (potentialUserIdentity.getPopulationId() == null) 1114 { 1115 for (UserPopulation up : userPopulations) 1116 { 1117 User user = _userManager.getUser(up, potentialUserIdentity.getLogin()); 1118 if (_isLoginCaseExact(user, potentialUserIdentity)) 1119 { 1120 return user.getIdentity(); 1121 } 1122 } 1123 } 1124 else 1125 { 1126 User user = _userManager.getUser(potentialUserIdentity.getPopulationId(), potentialUserIdentity.getLogin()); 1127 if (_isLoginCaseExact(user, potentialUserIdentity)) 1128 { 1129 return user.getIdentity(); 1130 } 1131 } 1132 1133 runningCredentialProvider.userNotAllowed(runningBlockingkMode, redirector); 1134 1135 if (getLogger().isWarnEnabled()) 1136 { 1137 getLogger().warn("The user '" + potentialUserIdentity + "' was authenticated by the credential provider '" + runningCredentialProvider.getCredentialProviderModelId() + "' but it does not match any user of the " + userPopulations.size() + " granted populations."); 1138 } 1139 1140 return null; 1141 } 1142 1143 private boolean _isLoginCaseExact(User user, UserIdentity potentialUserIdentity) 1144 { 1145 return user != null 1146 && (user.getUserDirectory().isCaseSensitive() && StringUtils.equals(user.getIdentity().getLogin(), potentialUserIdentity.getLogin()) 1147 || !user.getUserDirectory().isCaseSensitive() && StringUtils.equalsIgnoreCase(user.getIdentity().getLogin(), potentialUserIdentity.getLogin())); 1148 } 1149}