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