001/* 002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Thierry Delprat 018 * Bogdan Stefanescu 019 * Anahide Tchertchian 020 * Florent Guillaume 021 */ 022package org.nuxeo.ecm.platform.ui.web.auth; 023 024import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY; 025import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.ERROR_AUTHENTICATION_FAILED; 026import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.ERROR_CONNECTION_FAILED; 027import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORCE_ANONYMOUS_LOGIN; 028import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORM_SUBMITTED_MARKER; 029import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGINCONTEXT_KEY; 030import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_ERROR; 031import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_PAGE; 032import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_STATUS_CODE; 033import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGOUT_PAGE; 034import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.PAGE_AFTER_SWITCH; 035import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REDIRECT_URL; 036import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REQUESTED_URL; 037import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SECURITY_ERROR; 038import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SESSION_TIMEOUT; 039import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SSO_INITIAL_URL_REQUEST_KEY; 040import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.START_PAGE_SAVE_KEY; 041import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SWITCH_USER_KEY; 042import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SWITCH_USER_PAGE; 043import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.USERIDENT_KEY; 044 045import java.io.IOException; 046import java.io.Serializable; 047import java.io.UnsupportedEncodingException; 048import java.net.SocketException; 049import java.net.URLDecoder; 050import java.security.Principal; 051import java.util.ArrayList; 052import java.util.HashMap; 053import java.util.List; 054import java.util.Map; 055import java.util.concurrent.locks.ReentrantReadWriteLock; 056 057import javax.naming.NamingException; 058import javax.security.auth.callback.CallbackHandler; 059import javax.security.auth.login.LoginContext; 060import javax.security.auth.login.LoginException; 061import javax.servlet.Filter; 062import javax.servlet.FilterChain; 063import javax.servlet.FilterConfig; 064import javax.servlet.ServletException; 065import javax.servlet.ServletRequest; 066import javax.servlet.ServletResponse; 067import javax.servlet.http.Cookie; 068import javax.servlet.http.HttpServletRequest; 069import javax.servlet.http.HttpServletResponse; 070import javax.servlet.http.HttpServletResponseWrapper; 071import javax.servlet.http.HttpSession; 072import javax.ws.rs.core.Response; 073import javax.ws.rs.core.UriBuilder; 074 075import org.apache.commons.lang3.StringUtils; 076import org.apache.commons.lang3.exception.ExceptionUtils; 077import org.apache.commons.logging.Log; 078import org.apache.commons.logging.LogFactory; 079import org.nuxeo.common.utils.URIUtils; 080import org.nuxeo.ecm.core.api.NuxeoPrincipal; 081import org.nuxeo.ecm.core.api.SimplePrincipal; 082import org.nuxeo.ecm.core.api.local.ClientLoginModule; 083import org.nuxeo.ecm.core.event.EventContext; 084import org.nuxeo.ecm.core.event.EventProducer; 085import org.nuxeo.ecm.core.event.impl.UnboundEventContext; 086import org.nuxeo.ecm.directory.DirectoryException; 087import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo; 088import org.nuxeo.ecm.platform.api.login.UserIdentificationInfoCallbackHandler; 089import org.nuxeo.ecm.platform.login.PrincipalImpl; 090import org.nuxeo.ecm.platform.login.TrustingLoginPlugin; 091import org.nuxeo.ecm.platform.ui.web.auth.interfaces.LoginResponseHandler; 092import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthPreFilter; 093import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin; 094import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension; 095import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPropagator; 096import org.nuxeo.ecm.platform.ui.web.auth.service.AuthenticationPluginDescriptor; 097import org.nuxeo.ecm.platform.ui.web.auth.service.NuxeoAuthFilterChain; 098import org.nuxeo.ecm.platform.ui.web.auth.service.OpenUrlDescriptor; 099import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService; 100import org.nuxeo.ecm.platform.usermanager.UserManager; 101import org.nuxeo.ecm.platform.web.common.session.NuxeoHttpSessionMonitor; 102import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 103import org.nuxeo.runtime.api.Framework; 104import org.nuxeo.runtime.metrics.MetricsService; 105import org.nuxeo.runtime.model.ComponentManager; 106 107import com.codahale.metrics.Counter; 108import com.codahale.metrics.MetricRegistry; 109import com.codahale.metrics.SharedMetricRegistries; 110import com.codahale.metrics.Timer; 111 112/** 113 * Servlet filter handling Nuxeo authentication (JAAS + EJB). 114 * <p> 115 * Also handles logout and identity switch. 116 * 117 * @author Thierry Delprat 118 * @author Bogdan Stefanescu 119 * @author Anahide Tchertchian 120 * @author Florent Guillaume 121 */ 122public class NuxeoAuthenticationFilter implements Filter { 123 124 private static final Log log = LogFactory.getLog(NuxeoAuthenticationFilter.class); 125 126 // protected static final String EJB_LOGIN_DOMAIN = "nuxeo-system-login"; 127 128 /** 129 * @deprecated Since 8.4. Use {@link LoginScreenHelper#getStartupPagePath()} instead. 130 * @see LoginScreenHelper 131 */ 132 @Deprecated 133 public static final String DEFAULT_START_PAGE = "nxstartup.faces"; 134 135 /** 136 * LoginContext domain name in use by default in Nuxeo. 137 */ 138 public static final String LOGIN_DOMAIN = "nuxeo-ecm-web"; 139 140 protected static final String XMLHTTP_REQUEST_TYPE = "XMLHttpRequest"; 141 142 protected static final String LOGIN_JMS_CATEGORY = "NuxeoAuthentication"; 143 144 protected static volatile Boolean isLoginSynchronized; 145 146 /** Used internally as a marker. */ 147 protected static final Principal DIRECTORY_ERROR_PRINCIPAL = new PrincipalImpl("__DIRECTORY_ERROR__\0\0\0"); 148 149 private static String anonymous; 150 151 protected final boolean avoidReauthenticate = true; 152 153 protected volatile PluggableAuthenticationService service; 154 155 protected ReentrantReadWriteLock unAuthenticatedURLPrefixLock = new ReentrantReadWriteLock(); 156 157 protected List<String> unAuthenticatedURLPrefix; 158 159 /** 160 * On WebEngine (Jetty) we don't have JMS enabled so we should disable log 161 */ 162 protected boolean byPassAuthenticationLog = false; 163 164 /** 165 * Which security domain to use 166 */ 167 protected String securityDomain = LOGIN_DOMAIN; 168 169 // @since 5.7 170 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 171 172 protected final Timer requestTimer = registry.timer( 173 MetricRegistry.name("nuxeo", "web", "authentication", "requests", "count")); 174 175 protected final Counter concurrentCount = registry.counter( 176 MetricRegistry.name("nuxeo", "web", "authentication", "requests", "concurrent", "count")); 177 178 protected final Counter concurrentMaxCount = registry.counter( 179 MetricRegistry.name("nuxeo", "web", "authentication", "requests", "concurrent", "max")); 180 181 protected final Counter loginCount = registry.counter( 182 MetricRegistry.name("nuxeo", "web", "authentication", "logged-users")); 183 184 @Override 185 public void destroy() { 186 } 187 188 protected static boolean sendAuthenticationEvent(UserIdentificationInfo userInfo, String eventId, String comment) { 189 190 LoginContext loginContext = null; 191 try { 192 try { 193 loginContext = Framework.login(); 194 } catch (LoginException e) { 195 log.error("Unable to log in in order to log Login event" + e.getMessage()); 196 return false; 197 } 198 199 EventProducer evtProducer = Framework.getService(EventProducer.class); 200 Principal principal = new SimplePrincipal(userInfo.getUserName()); 201 202 Map<String, Serializable> props = new HashMap<>(); 203 props.put("AuthenticationPlugin", userInfo.getAuthPluginName()); 204 props.put("LoginPlugin", userInfo.getLoginPluginName()); 205 props.put("category", LOGIN_JMS_CATEGORY); 206 props.put("comment", comment); 207 208 EventContext ctx = new UnboundEventContext(principal, props); 209 evtProducer.fireEvent(ctx.newEvent(eventId)); 210 return true; 211 } finally { 212 if (loginContext != null) { 213 try { 214 loginContext.logout(); 215 } catch (LoginException e) { 216 log.error("Unable to logout: " + e.getMessage()); 217 } 218 } 219 } 220 } 221 222 protected boolean logAuthenticationAttempt(UserIdentificationInfo userInfo, boolean success) { 223 if (byPassAuthenticationLog) { 224 return true; 225 } 226 String userName = userInfo.getUserName(); 227 if (userName == null || userName.length() == 0) { 228 userName = userInfo.getToken(); 229 } 230 231 String eventId; 232 String comment; 233 if (success) { 234 eventId = "loginSuccess"; 235 comment = userName + " successfully logged in using " + userInfo.getAuthPluginName() + " authentication"; 236 loginCount.inc(); 237 } else { 238 eventId = "loginFailed"; 239 comment = userName + " failed to authenticate using " + userInfo.getAuthPluginName() + " authentication"; 240 } 241 242 return sendAuthenticationEvent(userInfo, eventId, comment); 243 } 244 245 protected boolean logLogout(UserIdentificationInfo userInfo) { 246 if (byPassAuthenticationLog) { 247 return true; 248 } 249 loginCount.dec(); 250 String userName = userInfo.getUserName(); 251 if (userName == null || userName.length() == 0) { 252 userName = userInfo.getToken(); 253 } 254 255 String eventId = "logout"; 256 String comment = userName + " logged out"; 257 258 return sendAuthenticationEvent(userInfo, eventId, comment); 259 } 260 261 protected static boolean isLoginSynchronized() { 262 if (isLoginSynchronized != null) { 263 return isLoginSynchronized; 264 } 265 if (Framework.getRuntime() == null) { 266 return false; 267 } 268 synchronized (NuxeoAuthenticationFilter.class) { 269 if (isLoginSynchronized != null) { 270 return isLoginSynchronized; 271 } 272 return isLoginSynchronized = !Boolean.parseBoolean(Framework.getProperty( 273 "org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter.isLoginNotSynchronized", "true")); 274 } 275 } 276 277 protected Principal doAuthenticate(CachableUserIdentificationInfo cachableUserIdent, 278 HttpServletRequest httpRequest) { 279 280 LoginContext loginContext; 281 try { 282 CallbackHandler handler = service.getCallbackHandler(cachableUserIdent.getUserInfo()); 283 loginContext = new LoginContext(securityDomain, handler); 284 285 if (isLoginSynchronized()) { 286 synchronized (NuxeoAuthenticationFilter.class) { 287 loginContext.login(); 288 } 289 } else { 290 loginContext.login(); 291 } 292 293 Principal principal = (Principal) loginContext.getSubject().getPrincipals().toArray()[0]; 294 cachableUserIdent.setPrincipal(principal); 295 cachableUserIdent.setAlreadyAuthenticated(true); 296 // re-set the userName since for some SSO based on token, 297 // the userName is not known before login is completed 298 cachableUserIdent.getUserInfo().setUserName(principal.getName()); 299 300 logAuthenticationAttempt(cachableUserIdent.getUserInfo(), true); 301 } catch (LoginException e) { 302 if (log.isInfoEnabled()) { 303 log.info(String.format("Login failed for %s on request %s", 304 cachableUserIdent.getUserInfo().getUserName(), httpRequest.getRequestURI())); 305 } 306 log.debug(e, e); 307 logAuthenticationAttempt(cachableUserIdent.getUserInfo(), false); 308 Throwable cause = e.getCause(); 309 if (cause instanceof DirectoryException) { 310 Throwable rootCause = ExceptionUtils.getRootCause(cause); 311 if (rootCause instanceof NamingException 312 && rootCause.getMessage().contains("LDAP response read timed out") 313 || rootCause instanceof SocketException) { 314 if (log.isDebugEnabled()) { 315 log.debug(String.format( 316 "Exception root cause is either a NamingException with \"LDAP response read timed out\"" 317 + " or a SocketException, setting the status code to %d," 318 + " more relevant than an illegitimate 401.", 319 HttpServletResponse.SC_GATEWAY_TIMEOUT)); 320 } 321 httpRequest.setAttribute(LOGIN_STATUS_CODE, HttpServletResponse.SC_GATEWAY_TIMEOUT); 322 } 323 return DIRECTORY_ERROR_PRINCIPAL; 324 } 325 return null; 326 } 327 328 // store login context for the time of the request 329 // TODO logincontext is also stored in cachableUserIdent - it is really 330 // needed to store it?? 331 httpRequest.setAttribute(LOGINCONTEXT_KEY, loginContext); 332 333 // store user ident 334 cachableUserIdent.setLoginContext(loginContext); 335 boolean createSession = needSessionSaving(cachableUserIdent.getUserInfo()); 336 HttpSession session = httpRequest.getSession(createSession); 337 if (session != null) { 338 session.setAttribute(USERIDENT_KEY, cachableUserIdent); 339 } 340 341 service.onAuthenticatedSessionCreated(httpRequest, session, cachableUserIdent); 342 343 return cachableUserIdent.getPrincipal(); 344 } 345 346 private boolean switchUser(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException { 347 HttpServletRequest httpRequest = (HttpServletRequest) request; 348 349 String deputyLogin = (String) httpRequest.getAttribute(SWITCH_USER_KEY); 350 String targetPageAfterSwitch = (String) httpRequest.getAttribute(PAGE_AFTER_SWITCH); 351 if (targetPageAfterSwitch == null) { 352 targetPageAfterSwitch = LoginScreenHelper.getStartupPagePath(); 353 } 354 355 CachableUserIdentificationInfo cachableUserIdent = retrieveIdentityFromCache(httpRequest); 356 String originatingUser = cachableUserIdent.getUserInfo().getUserName(); 357 358 if (deputyLogin == null) { 359 // simply switch back to the previous identity 360 NuxeoPrincipal currentPrincipal = (NuxeoPrincipal) cachableUserIdent.getPrincipal(); 361 String previousUser = currentPrincipal.getOriginatingUser(); 362 if (previousUser == null) { 363 return false; 364 } 365 deputyLogin = previousUser; 366 originatingUser = null; 367 } 368 369 try { 370 cachableUserIdent.getLoginContext().logout(); 371 } catch (LoginException e1) { 372 log.error("Error while logout from main identity", e1); 373 } 374 375 httpRequest.getSession(false); 376 service.reinitSession(httpRequest); 377 378 CachableUserIdentificationInfo newCachableUserIdent = new CachableUserIdentificationInfo(deputyLogin, 379 deputyLogin); 380 381 newCachableUserIdent.getUserInfo().setLoginPluginName(TrustingLoginPlugin.NAME); 382 newCachableUserIdent.getUserInfo().setAuthPluginName(cachableUserIdent.getUserInfo().getAuthPluginName()); 383 384 Principal principal = doAuthenticate(newCachableUserIdent, httpRequest); 385 if (principal != null && principal != DIRECTORY_ERROR_PRINCIPAL) { 386 NuxeoPrincipal nxUser = (NuxeoPrincipal) principal; 387 if (originatingUser != null) { 388 nxUser.setOriginatingUser(originatingUser); 389 } 390 propagateUserIdentificationInformation(cachableUserIdent); 391 } 392 393 // reinit Seam so the afterResponseComplete does not crash 394 // ServletLifecycle.beginRequest(httpRequest); 395 396 // flag redirect to avoid being caught by URLPolicy 397 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, Boolean.TRUE); 398 String baseURL = service.getBaseURL(request); 399 ((HttpServletResponse) response).sendRedirect(baseURL + targetPageAfterSwitch); 400 401 return true; 402 } 403 404 @Override 405 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 406 throws IOException, ServletException { 407 final Timer.Context contextTimer = requestTimer.time(); 408 concurrentCount.inc(); 409 if (concurrentCount.getCount() > concurrentMaxCount.getCount()) { 410 concurrentMaxCount.inc(); 411 } 412 try { 413 doInitIfNeeded(); 414 415 List<NuxeoAuthPreFilter> preFilters = service.getPreFilters(); 416 417 if (preFilters == null) { 418 doFilterInternal(request, response, chain); 419 } else { 420 NuxeoAuthFilterChain chainWithPreFilters = new NuxeoAuthFilterChain(preFilters, chain, this); 421 chainWithPreFilters.doFilter(request, response); 422 } 423 } finally { 424 ClientLoginModule.clearThreadLocalLogin(); 425 contextTimer.stop(); 426 concurrentCount.dec(); 427 } 428 } 429 430 public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) 431 throws IOException, ServletException { 432 433 if (bypassAuth((HttpServletRequest) request)) { 434 chain.doFilter(request, response); 435 return; 436 } 437 438 String tokenPage = getRequestedPage(request); 439 if (tokenPage.equals(SWITCH_USER_PAGE)) { 440 boolean result = switchUser(request, response, chain); 441 if (result) { 442 return; 443 } 444 } 445 446 if (request instanceof NuxeoSecuredRequestWrapper) { 447 log.debug("ReEntering Nuxeo Authentication Filter ... exiting directly"); 448 chain.doFilter(request, response); 449 return; 450 } else if (service.canBypassRequest(request)) { 451 log.debug("ReEntering Nuxeo Authentication Filter after URL rewrite ... exiting directly"); 452 chain.doFilter(request, response); 453 return; 454 } else { 455 log.debug("Entering Nuxeo Authentication Filter"); 456 } 457 458 String targetPageURL = null; 459 HttpServletRequest httpRequest = (HttpServletRequest) request; 460 HttpServletResponse httpResponse = (HttpServletResponse) response; 461 Principal principal = httpRequest.getUserPrincipal(); 462 463 NuxeoAuthenticationPropagator.CleanupCallback propagatedAuthCb = null; 464 465 String forceAnonymousLoginParam = httpRequest.getParameter(FORCE_ANONYMOUS_LOGIN); 466 boolean forceAnonymousLogin = Boolean.parseBoolean(forceAnonymousLoginParam); 467 468 try { 469 if (principal == null) { 470 log.debug("Principal not found inside Request via getUserPrincipal"); 471 // need to authenticate ! 472 473 // retrieve user & password 474 CachableUserIdentificationInfo cachableUserIdent; 475 if (avoidReauthenticate) { 476 log.debug("Try getting authentication from cache"); 477 cachableUserIdent = retrieveIdentityFromCache(httpRequest); 478 } else { 479 log.debug("Principal cache is NOT activated"); 480 } 481 482 if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null) { 483 if (cachableUserIdent.getUserInfo().getUserName().equals(getAnonymousId())) { 484 if (forceAnonymousLogin) { 485 cachableUserIdent = null; 486 } 487 } 488 489 if (service.needResetLogin(request)) { 490 HttpSession session = httpRequest.getSession(false); 491 if (session != null) { 492 session.removeAttribute(USERIDENT_KEY); 493 } 494 // first propagate the login because invalidation may 495 // require 496 // an authenticated session 497 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 498 // invalidate Session ! 499 try { 500 service.invalidateSession(request); 501 } finally { 502 if (propagatedAuthCb != null) { 503 propagatedAuthCb.cleanup(); 504 propagatedAuthCb = null; 505 } 506 } 507 // TODO perform logout? 508 cachableUserIdent = null; 509 } 510 } 511 512 // identity found in cache 513 if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null) { 514 log.debug("userIdent found in cache, get the Principal from it without reloggin"); 515 516 NuxeoHttpSessionMonitor.instance().updateEntry(httpRequest); 517 518 principal = cachableUserIdent.getPrincipal(); 519 log.debug("Principal = " + principal.getName()); 520 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 521 522 String requestedPage = getRequestedPage(httpRequest); 523 if (LOGOUT_PAGE.equals(requestedPage)) { 524 boolean redirected = handleLogout(request, response, cachableUserIdent); 525 cachableUserIdent = null; 526 principal = null; 527 if (redirected && httpRequest.getParameter(FORM_SUBMITTED_MARKER) == null) { 528 return; 529 } 530 } else if (LOGIN_PAGE.equals(requestedPage)) { 531 if (handleLogin(httpRequest, httpResponse)) { 532 return; 533 } 534 } else { 535 targetPageURL = getSavedRequestedURL(httpRequest, httpResponse); 536 } 537 } 538 539 // identity not found in cache or reseted by logout 540 if (cachableUserIdent == null || cachableUserIdent.getUserInfo() == null) { 541 UserIdentificationInfo userIdent = handleRetrieveIdentity(httpRequest, httpResponse); 542 if (userIdent != null && userIdent.containsValidIdentity() 543 && userIdent.getUserName().equals(getAnonymousId())) { 544 if (forceAnonymousLogin) { 545 userIdent = null; 546 } 547 } 548 if ((userIdent == null || !userIdent.containsValidIdentity()) && !bypassAuth(httpRequest)) { 549 boolean res = handleLoginPrompt(httpRequest, httpResponse); 550 if (res) { 551 return; 552 } 553 } else { 554 String redirectUrl = VirtualHostHelper.getRedirectUrl(httpRequest); 555 HttpSession session = httpRequest.getSession(false); 556 if (session != null) { 557 session.setAttribute(REDIRECT_URL, redirectUrl); 558 } 559 // restore saved Starting page 560 targetPageURL = getSavedRequestedURL(httpRequest, httpResponse); 561 } 562 if (userIdent != null && userIdent.containsValidIdentity()) { 563 // do the authentication 564 cachableUserIdent = new CachableUserIdentificationInfo(userIdent); 565 principal = doAuthenticate(cachableUserIdent, httpRequest); 566 if (principal != null && principal != DIRECTORY_ERROR_PRINCIPAL) { 567 // Do the propagation too ???? 568 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 569 // setPrincipalToSession(httpRequest, principal); 570 // check if the current authenticator is a 571 // LoginResponseHandler 572 NuxeoAuthenticationPlugin plugin = getAuthenticator(cachableUserIdent); 573 if (plugin instanceof LoginResponseHandler) { 574 // call the extended error handler 575 if (((LoginResponseHandler) plugin).onSuccess((HttpServletRequest) request, 576 (HttpServletResponse) response)) { 577 return; 578 } 579 } 580 } else { 581 // first check if the current authenticator is a 582 // LoginResponseHandler 583 NuxeoAuthenticationPlugin plugin = getAuthenticator(cachableUserIdent); 584 if (plugin instanceof LoginResponseHandler) { 585 // call the extended error handler 586 if (((LoginResponseHandler) plugin).onError((HttpServletRequest) request, 587 (HttpServletResponse) response)) { 588 return; 589 } 590 } else { 591 // use the old method 592 String err = principal == DIRECTORY_ERROR_PRINCIPAL ? ERROR_CONNECTION_FAILED 593 : ERROR_AUTHENTICATION_FAILED; 594 httpRequest.setAttribute(LOGIN_ERROR, err); 595 boolean res = handleLoginPrompt(httpRequest, httpResponse); 596 if (res) { 597 return; 598 } 599 } 600 } 601 602 } 603 } 604 } 605 606 if (principal != null) { 607 if (targetPageURL != null && targetPageURL.length() > 0) { 608 // forward to target page 609 String baseURL = service.getBaseURL(request); 610 611 // httpRequest.getRequestDispatcher(targetPageURL).forward(new 612 // NuxeoSecuredRequestWrapper(httpRequest, principal), 613 // response); 614 if (XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) { 615 // httpResponse.setStatus(200); 616 return; 617 } else { 618 // In case of a download redirection, the base url is already contained in the target 619 String url = targetPageURL.startsWith(baseURL) ? targetPageURL : baseURL + targetPageURL; 620 httpResponse.sendRedirect(url); 621 return; 622 } 623 624 } else { 625 // simply continue request 626 chain.doFilter(new NuxeoSecuredRequestWrapper(httpRequest, principal), response); 627 } 628 } else { 629 chain.doFilter(request, response); 630 } 631 } finally { 632 if (propagatedAuthCb != null) { 633 propagatedAuthCb.cleanup(); 634 } 635 } 636 if (!avoidReauthenticate) { 637 // destroy login context 638 log.debug("Log out"); 639 LoginContext lc = (LoginContext) httpRequest.getAttribute("LoginContext"); 640 if (lc != null) { 641 try { 642 lc.logout(); 643 } catch (LoginException e) { 644 log.error(e, e); 645 } 646 } 647 } 648 log.debug("Exit Nuxeo Authentication filter"); 649 } 650 651 public NuxeoAuthenticationPlugin getAuthenticator(CachableUserIdentificationInfo ci) { 652 String key = ci.getUserInfo().getAuthPluginName(); 653 if (key != null) { 654 NuxeoAuthenticationPlugin authPlugin = service.getPlugin(key); 655 return authPlugin; 656 } 657 return null; 658 } 659 660 protected static CachableUserIdentificationInfo retrieveIdentityFromCache(HttpServletRequest httpRequest) { 661 662 HttpSession session = httpRequest.getSession(false); 663 if (session != null) { 664 CachableUserIdentificationInfo cachableUserInfo = (CachableUserIdentificationInfo) session.getAttribute( 665 USERIDENT_KEY); 666 if (cachableUserInfo != null) { 667 return cachableUserInfo; 668 } 669 } 670 671 return null; 672 } 673 674 private String getAnonymousId() throws ServletException { 675 if (anonymous == null) { 676 anonymous = Framework.getService(UserManager.class).getAnonymousUserId(); 677 } 678 return anonymous; 679 } 680 681 protected void doInitIfNeeded() throws ServletException { 682 if (service == null && Framework.getRuntime() != null) { 683 synchronized (this) { 684 if (service != null) { 685 return; 686 } 687 PluggableAuthenticationService svc = (PluggableAuthenticationService) Framework.getRuntime() 688 .getComponent( 689 PluggableAuthenticationService.NAME); 690 svc.initPreFilters(); 691 new ComponentManager.Listener() { 692 // nullify service field if components are restarting 693 @Override 694 public void beforeStart(ComponentManager mgr, boolean isResume) { 695 service = null; 696 uninstall(); 697 } 698 }.install(); 699 service = svc; 700 } 701 } 702 } 703 704 @Override 705 public void init(FilterConfig config) throws ServletException { 706 String val = config.getInitParameter("byPassAuthenticationLog"); 707 if (val != null && Boolean.parseBoolean(val)) { 708 byPassAuthenticationLog = true; 709 } 710 val = config.getInitParameter("securityDomain"); 711 if (val != null) { 712 securityDomain = val; 713 } 714 715 } 716 717 /** 718 * Save requested URL before redirecting to login form. 719 * <p> 720 * Returns true if target url is a valid startup page. 721 */ 722 public boolean saveRequestedURLBeforeRedirect(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 723 724 final boolean hasRequestedSessionId = !StringUtils.isBlank(httpRequest.getRequestedSessionId()); 725 726 HttpSession session = httpRequest.getSession(false); 727 final boolean isTimeout = session == null && hasRequestedSessionId; 728 729 if (!httpResponse.isCommitted()) { 730 session = httpRequest.getSession(true); 731 } 732 733 if (session == null) { 734 return false; 735 } 736 737 String requestPage; 738 boolean requestPageInParams = false; 739 if (httpRequest.getParameter(REQUESTED_URL) != null) { 740 requestPageInParams = true; 741 requestPage = httpRequest.getParameter(REQUESTED_URL); 742 } else { 743 requestPage = getRequestedUrl(httpRequest); 744 } 745 746 if (requestPage == null) { 747 return false; 748 } 749 750 // add a flag to tell that the Session looks like having timed out 751 if (isTimeout && !requestPage.equals(LoginScreenHelper.getStartupPagePath())) { 752 session.setAttribute(SESSION_TIMEOUT, Boolean.TRUE); 753 } else { 754 session.removeAttribute(SESSION_TIMEOUT); 755 } 756 757 // avoid saving to session is start page is not valid or if it's 758 // already in the request params 759 if (isStartPageValid(requestPage)) { 760 if (!requestPageInParams) { 761 session.setAttribute(START_PAGE_SAVE_KEY, requestPage); 762 } 763 return true; 764 } 765 766 // avoid redirect if not useful 767 for (String startupPagePath : LoginScreenHelper.getStartupPagePaths()) { 768 if (requestPage.startsWith(startupPagePath) 769 && LoginScreenHelper.getStartupPagePath().equals(startupPagePath)) { 770 return true; 771 } 772 } 773 774 return false; 775 } 776 777 public static String getRequestedUrl(HttpServletRequest httpRequest) { 778 String completeURI = httpRequest.getRequestURI(); 779 String qs = httpRequest.getQueryString(); 780 String context = httpRequest.getContextPath() + '/'; 781 String requestPage = completeURI.substring(context.length()); 782 if (qs != null && qs.length() > 0) { 783 // remove conversationId if present 784 if (qs.contains("conversationId")) { 785 qs = qs.replace("conversationId", "old_conversationId"); 786 } 787 requestPage = requestPage + '?' + qs; 788 } 789 return requestPage; 790 } 791 792 protected static String getSavedRequestedURL(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 793 794 HttpSession session = httpRequest.getSession(false); 795 String requestedPage = httpRequest.getParameter(REQUESTED_URL); 796 if (StringUtils.isBlank(requestedPage)) { 797 // retrieve from session 798 if (session != null) { 799 requestedPage = (String) session.getAttribute(START_PAGE_SAVE_KEY); 800 } 801 802 // retrieve from SSO cookies 803 Cookie[] cookies = httpRequest.getCookies(); 804 if (cookies != null) { 805 for (Cookie cookie : cookies) { 806 if (SSO_INITIAL_URL_REQUEST_KEY.equals(cookie.getName())) { 807 requestedPage = cookie.getValue(); 808 cookie.setPath("/"); 809 // enforce cookie removal 810 cookie.setMaxAge(0); 811 httpResponse.addCookie(cookie); 812 } 813 } 814 } 815 } 816 817 if (requestedPage != null) { 818 // retrieve URL fragment from cookie 819 Cookie[] cookies = httpRequest.getCookies(); 820 if (cookies != null) { 821 for (Cookie cookie : cookies) { 822 if (NXAuthConstants.START_PAGE_FRAGMENT_KEY.equals(cookie.getName())) { 823 try { 824 requestedPage = UriBuilder.fromUri(requestedPage) 825 .fragment(URLDecoder.decode(cookie.getValue(), "UTF-8")) 826 .build() 827 .toString(); 828 } catch (UnsupportedEncodingException e) { 829 log.error("Failed to decode start page url fragment", e); 830 } 831 // enforce cookie removal 832 cookie.setMaxAge(0); 833 httpResponse.addCookie(cookie); 834 } 835 } 836 } 837 } 838 839 // clean up session 840 if (session != null) { 841 session.removeAttribute(START_PAGE_SAVE_KEY); 842 } 843 844 return requestedPage; 845 } 846 847 protected boolean isStartPageValid(String startPage) { 848 if (startPage == null) { 849 return false; 850 } 851 try { 852 // Sometimes, the service is not initialized at startup 853 doInitIfNeeded(); 854 } catch (ServletException e) { 855 return false; 856 } 857 for (String prefix : service.getStartURLPatterns()) { 858 if (startPage.startsWith(prefix)) { 859 return true; 860 } 861 } 862 return false; 863 } 864 865 protected boolean handleLogout(ServletRequest request, ServletResponse response, 866 CachableUserIdentificationInfo cachedUserInfo) throws ServletException { 867 logLogout(cachedUserInfo.getUserInfo()); 868 869 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, Boolean.TRUE); 870 Map<String, String> parameters = new HashMap<>(); 871 String securityError = request.getParameter(SECURITY_ERROR); 872 if (securityError != null) { 873 parameters.put(SECURITY_ERROR, securityError); 874 } 875 if (cachedUserInfo.getPrincipal().getName().equals(getAnonymousId())) { 876 parameters.put(FORCE_ANONYMOUS_LOGIN, "true"); 877 } 878 String requestedUrl = request.getParameter(REQUESTED_URL); 879 if (requestedUrl != null) { 880 parameters.put(REQUESTED_URL, requestedUrl); 881 } 882 // Reset JSESSIONID Cookie 883 HttpServletResponse httpResponse = (HttpServletResponse) response; 884 Cookie cookie = new Cookie("JSESSIONID", null); 885 cookie.setMaxAge(0); 886 cookie.setPath("/"); 887 httpResponse.addCookie(cookie); 888 889 String pluginName = cachedUserInfo.getUserInfo().getAuthPluginName(); 890 NuxeoAuthenticationPlugin authPlugin = service.getPlugin(pluginName); 891 NuxeoAuthenticationPluginLogoutExtension logoutPlugin = null; 892 893 if (authPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) { 894 logoutPlugin = (NuxeoAuthenticationPluginLogoutExtension) authPlugin; 895 } 896 897 boolean redirected = false; 898 if (logoutPlugin != null) { 899 redirected = Boolean.TRUE.equals( 900 logoutPlugin.handleLogout((HttpServletRequest) request, (HttpServletResponse) response)); 901 } 902 903 // invalidate Session ! 904 service.invalidateSession(request); 905 906 HttpServletRequest httpRequest = (HttpServletRequest) request; 907 if (!redirected && !XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) { 908 String baseURL = service.getBaseURL(request); 909 try { 910 String url = baseURL + LoginScreenHelper.getStartupPagePath(); 911 url = URIUtils.addParametersToURIQuery(url, parameters); 912 ((HttpServletResponse) response).sendRedirect(url); 913 redirected = true; 914 } catch (IOException e) { 915 log.error("Unable to redirect to default start page after logout : " + e.getMessage()); 916 } 917 } 918 919 try { 920 cachedUserInfo.getLoginContext().logout(); 921 } catch (LoginException e) { 922 log.error("Unable to logout " + e.getMessage()); 923 } 924 return redirected; 925 } 926 927 // App Server JAAS SPI 928 protected void propagateUserIdentificationInformation(CachableUserIdentificationInfo cachableUserIdent) { 929 service.propagateUserIdentificationInformation(cachableUserIdent); 930 } 931 932 // Plugin API 933 protected void initUnAuthenticatedURLPrefix() { 934 // gather unAuthenticated URLs 935 unAuthenticatedURLPrefix = new ArrayList<>(); 936 for (String pluginName : service.getAuthChain()) { 937 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 938 List<String> prefix = plugin.getUnAuthenticatedURLPrefix(); 939 if (prefix != null && !prefix.isEmpty()) { 940 unAuthenticatedURLPrefix.addAll(prefix); 941 } 942 } 943 } 944 945 protected boolean bypassAuth(HttpServletRequest httpRequest) { 946 if (unAuthenticatedURLPrefix == null) { 947 try { 948 unAuthenticatedURLPrefixLock.writeLock().lock(); 949 // late init to allow plugins registered after this filter init 950 initUnAuthenticatedURLPrefix(); 951 } finally { 952 unAuthenticatedURLPrefixLock.writeLock().unlock(); 953 } 954 } 955 956 try { 957 unAuthenticatedURLPrefixLock.readLock().lock(); 958 String requestPage = getRequestedPage(httpRequest); 959 for (String prefix : unAuthenticatedURLPrefix) { 960 if (requestPage.startsWith(prefix)) { 961 return true; 962 } 963 } 964 } finally { 965 unAuthenticatedURLPrefixLock.readLock().unlock(); 966 } 967 968 List<OpenUrlDescriptor> openUrls = service.getOpenUrls(); 969 for (OpenUrlDescriptor openUrl : openUrls) { 970 if (openUrl.allowByPassAuth(httpRequest)) { 971 return true; 972 } 973 } 974 975 return false; 976 } 977 978 public static String getRequestedPage(ServletRequest request) { 979 if (request instanceof HttpServletRequest) { 980 HttpServletRequest httpRequest = (HttpServletRequest) request; 981 return getRequestedPage(httpRequest); 982 } else { 983 return null; 984 } 985 } 986 987 protected static String getRequestedPage(HttpServletRequest httpRequest) { 988 String path = httpRequest.getServletPath(); // use decoded and normalized servlet path 989 String info = httpRequest.getPathInfo(); 990 if (info != null) { 991 path = path + info; 992 } 993 if (!path.isEmpty()) { 994 path = path.substring(1); // strip initial / 995 } 996 return path; 997 } 998 999 protected boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 1000 1001 // A specific auth chain may prevent the filter to relay to a login prompt. 1002 if (!service.doHandlePrompt(httpRequest)) { 1003 buildUnauthorizedResponse(httpRequest, httpResponse); 1004 return true; 1005 } 1006 1007 return handleLogin(httpRequest, httpResponse); 1008 1009 } 1010 1011 private boolean handleLogin(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 1012 String baseURL = service.getBaseURL(httpRequest); 1013 1014 // go through plugins to get UserIndentity 1015 for (String pluginName : service.getAuthChain(httpRequest)) { 1016 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 1017 AuthenticationPluginDescriptor descriptor = service.getDescriptor(pluginName); 1018 1019 if (Boolean.TRUE.equals(plugin.needLoginPrompt(httpRequest))) { 1020 if (descriptor.getNeedStartingURLSaving()) { 1021 saveRequestedURLBeforeRedirect(httpRequest, httpResponse); 1022 } 1023 1024 HttpServletResponse response = new HttpServletResponseWrapper(httpResponse) { 1025 @Override 1026 public void sendRedirect(String location) throws IOException { 1027 HttpServletResponse response = (HttpServletResponse) getResponse(); 1028 StringBuilder sb = new StringBuilder(); 1029 sb.append("<script type=\"text/javascript\">\n"); 1030 sb.append("document.cookie = '" + NXAuthConstants.START_PAGE_FRAGMENT_KEY 1031 + "=' + encodeURIComponent(window.location.hash.substring(1) || '') + '; path=/';\n"); 1032 sb.append("window.location = '" + location + "';\n"); 1033 sb.append("</script>"); 1034 String script = sb.toString(); 1035 1036 response.setStatus(SC_UNAUTHORIZED); 1037 response.setContentType("text/html;charset=UTF-8"); 1038 response.setContentLength(script.length()); 1039 response.getWriter().write(script); 1040 } 1041 }; 1042 return Boolean.TRUE.equals(plugin.handleLoginPrompt(httpRequest, response, baseURL)); 1043 } 1044 } 1045 1046 log.warn("No auth plugin can be found to do the Login Prompt"); 1047 return false; 1048 } 1049 1050 private void buildUnauthorizedResponse(HttpServletRequest req, HttpServletResponse resp) { 1051 1052 try { 1053 String loginUrl = VirtualHostHelper.getBaseURL(req) + LOGIN_PAGE; 1054 resp.addHeader("Location", loginUrl); 1055 resp.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); 1056 resp.getWriter().write("Please log in at: " + loginUrl); 1057 } catch (IOException e) { 1058 log.error("Unable to write login page on unauthorized response", e); 1059 } 1060 } 1061 1062 protected UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, 1063 HttpServletResponse httpResponse) { 1064 1065 UserIdentificationInfo userIdent = null; 1066 1067 // go through plugins to get UserIdentity 1068 for (String pluginName : service.getAuthChain(httpRequest)) { 1069 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 1070 if (plugin != null) { 1071 log.debug("Trying to retrieve userIdentification using plugin " + pluginName); 1072 userIdent = plugin.handleRetrieveIdentity(httpRequest, httpResponse); 1073 if (userIdent != null && userIdent.containsValidIdentity()) { 1074 // fill information for the Login module 1075 userIdent.setAuthPluginName(pluginName); 1076 1077 // get the target login module 1078 String loginModulePlugin = service.getDescriptor(pluginName).getLoginModulePlugin(); 1079 userIdent.setLoginPluginName(loginModulePlugin); 1080 1081 // get the additional parameters 1082 Map<String, String> parameters = service.getDescriptor(pluginName).getParameters(); 1083 if (userIdent.getLoginParameters() != null) { 1084 // keep existing parameters set by the auth plugin 1085 if (parameters == null) { 1086 parameters = new HashMap<>(); 1087 } 1088 parameters.putAll(userIdent.getLoginParameters()); 1089 } 1090 userIdent.setLoginParameters(parameters); 1091 1092 break; 1093 } 1094 } else { 1095 log.error("Auth plugin " + pluginName + " can not be retrieved from service"); 1096 } 1097 } 1098 1099 // Fall back to cache (used only when avoidReautenticated=false) 1100 if (userIdent == null || !userIdent.containsValidIdentity()) { 1101 log.debug("user/password not found in request, try into identity cache"); 1102 HttpSession session = httpRequest.getSession(false); 1103 if (session == null) { 1104 // possible we need a new session 1105 if (httpRequest.isRequestedSessionIdValid()) { 1106 session = httpRequest.getSession(true); 1107 } 1108 } 1109 if (session != null) { 1110 CachableUserIdentificationInfo savedUserInfo = retrieveIdentityFromCache(httpRequest); 1111 if (savedUserInfo != null) { 1112 log.debug("Found User identity in cache :" + savedUserInfo.getUserInfo().getUserName()); 1113 userIdent = new UserIdentificationInfo(savedUserInfo.getUserInfo()); 1114 savedUserInfo.setPrincipal(null); 1115 } 1116 } 1117 } else { 1118 log.debug("User/Password found as parameter of the request"); 1119 } 1120 1121 return userIdent; 1122 } 1123 1124 protected boolean needSessionSaving(UserIdentificationInfo userInfo) { 1125 String pluginName = userInfo.getAuthPluginName(); 1126 1127 AuthenticationPluginDescriptor desc = service.getDescriptor(pluginName); 1128 1129 if (desc.getStateful()) { 1130 return true; 1131 } else { 1132 return desc.getNeedStartingURLSaving(); 1133 } 1134 } 1135 1136 /** 1137 * Does a forced login as the given user. Bypasses all authentication checks. 1138 * 1139 * @param username the user name 1140 * @return the login context, which MUST be used for logout in a {@code finally} block 1141 * @throws LoginException 1142 */ 1143 public static LoginContext loginAs(String username) throws LoginException { 1144 UserIdentificationInfo userIdent = new UserIdentificationInfo(username, ""); 1145 userIdent.setLoginPluginName(TrustingLoginPlugin.NAME); 1146 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 1147 .getComponent( 1148 PluggableAuthenticationService.NAME); 1149 CallbackHandler callbackHandler; 1150 if (authService != null) { 1151 callbackHandler = authService.getCallbackHandler(userIdent); 1152 } else { 1153 callbackHandler = new UserIdentificationInfoCallbackHandler(userIdent); 1154 } 1155 LoginContext loginContext = new LoginContext(LOGIN_DOMAIN, callbackHandler); 1156 1157 if (isLoginSynchronized()) { 1158 synchronized (NuxeoAuthenticationFilter.class) { 1159 loginContext.login(); 1160 } 1161 } else { 1162 loginContext.login(); 1163 } 1164 return loginContext; 1165 } 1166 1167}