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.lang.StringUtils; 076import org.apache.commons.lang.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 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 log.info("Login failed for " + cachableUserIdent.getUserInfo().getUserName()); 303 logAuthenticationAttempt(cachableUserIdent.getUserInfo(), false); 304 Throwable cause = e.getCause(); 305 if (cause instanceof DirectoryException) { 306 Throwable rootCause = ExceptionUtils.getRootCause(cause); 307 if (rootCause instanceof NamingException 308 && rootCause.getMessage().contains("LDAP response read timed out") 309 || rootCause instanceof SocketException) { 310 httpRequest.setAttribute(LOGIN_STATUS_CODE, HttpServletResponse.SC_GATEWAY_TIMEOUT); 311 } 312 return DIRECTORY_ERROR_PRINCIPAL; 313 } 314 return null; 315 } 316 317 // store login context for the time of the request 318 // TODO logincontext is also stored in cachableUserIdent - it is really 319 // needed to store it?? 320 httpRequest.setAttribute(LOGINCONTEXT_KEY, loginContext); 321 322 // store user ident 323 cachableUserIdent.setLoginContext(loginContext); 324 boolean createSession = needSessionSaving(cachableUserIdent.getUserInfo()); 325 HttpSession session = httpRequest.getSession(createSession); 326 if (session != null) { 327 session.setAttribute(USERIDENT_KEY, cachableUserIdent); 328 } 329 330 service.onAuthenticatedSessionCreated(httpRequest, session, cachableUserIdent); 331 332 return cachableUserIdent.getPrincipal(); 333 } 334 335 private boolean switchUser(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException { 336 HttpServletRequest httpRequest = (HttpServletRequest) request; 337 338 String deputyLogin = (String) httpRequest.getAttribute(SWITCH_USER_KEY); 339 String targetPageAfterSwitch = (String) httpRequest.getAttribute(PAGE_AFTER_SWITCH); 340 if (targetPageAfterSwitch == null) { 341 targetPageAfterSwitch = LoginScreenHelper.getStartupPagePath(); 342 } 343 344 CachableUserIdentificationInfo cachableUserIdent = retrieveIdentityFromCache(httpRequest); 345 String originatingUser = cachableUserIdent.getUserInfo().getUserName(); 346 347 if (deputyLogin == null) { 348 // simply switch back to the previous identity 349 NuxeoPrincipal currentPrincipal = (NuxeoPrincipal) cachableUserIdent.getPrincipal(); 350 String previousUser = currentPrincipal.getOriginatingUser(); 351 if (previousUser == null) { 352 return false; 353 } 354 deputyLogin = previousUser; 355 originatingUser = null; 356 } 357 358 try { 359 cachableUserIdent.getLoginContext().logout(); 360 } catch (LoginException e1) { 361 log.error("Error while logout from main identity", e1); 362 } 363 364 httpRequest.getSession(false); 365 service.reinitSession(httpRequest); 366 367 CachableUserIdentificationInfo newCachableUserIdent = new CachableUserIdentificationInfo(deputyLogin, 368 deputyLogin); 369 370 newCachableUserIdent.getUserInfo().setLoginPluginName(TrustingLoginPlugin.NAME); 371 newCachableUserIdent.getUserInfo().setAuthPluginName(cachableUserIdent.getUserInfo().getAuthPluginName()); 372 373 Principal principal = doAuthenticate(newCachableUserIdent, httpRequest); 374 if (principal != null && principal != DIRECTORY_ERROR_PRINCIPAL) { 375 NuxeoPrincipal nxUser = (NuxeoPrincipal) principal; 376 if (originatingUser != null) { 377 nxUser.setOriginatingUser(originatingUser); 378 } 379 propagateUserIdentificationInformation(cachableUserIdent); 380 } 381 382 // reinit Seam so the afterResponseComplete does not crash 383 // ServletLifecycle.beginRequest(httpRequest); 384 385 // flag redirect to avoid being caught by URLPolicy 386 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, Boolean.TRUE); 387 String baseURL = service.getBaseURL(request); 388 ((HttpServletResponse) response).sendRedirect(baseURL + targetPageAfterSwitch); 389 390 return true; 391 } 392 393 @Override 394 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 395 throws IOException, ServletException { 396 final Timer.Context contextTimer = requestTimer.time(); 397 concurrentCount.inc(); 398 if (concurrentCount.getCount() > concurrentMaxCount.getCount()) { 399 concurrentMaxCount.inc(); 400 } 401 try { 402 doInitIfNeeded(); 403 404 List<NuxeoAuthPreFilter> preFilters = service.getPreFilters(); 405 406 if (preFilters == null) { 407 doFilterInternal(request, response, chain); 408 } else { 409 NuxeoAuthFilterChain chainWithPreFilters = new NuxeoAuthFilterChain(preFilters, chain, this); 410 chainWithPreFilters.doFilter(request, response); 411 } 412 } finally { 413 ClientLoginModule.clearThreadLocalLogin(); 414 contextTimer.stop(); 415 concurrentCount.dec(); 416 } 417 } 418 419 public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) 420 throws IOException, ServletException { 421 422 if (bypassAuth((HttpServletRequest) request)) { 423 chain.doFilter(request, response); 424 return; 425 } 426 427 String tokenPage = getRequestedPage(request); 428 if (tokenPage.equals(SWITCH_USER_PAGE)) { 429 boolean result = switchUser(request, response, chain); 430 if (result) { 431 return; 432 } 433 } 434 435 if (request instanceof NuxeoSecuredRequestWrapper) { 436 log.debug("ReEntering Nuxeo Authentication Filter ... exiting directly"); 437 chain.doFilter(request, response); 438 return; 439 } else if (service.canBypassRequest(request)) { 440 log.debug("ReEntering Nuxeo Authentication Filter after URL rewrite ... exiting directly"); 441 chain.doFilter(request, response); 442 return; 443 } else { 444 log.debug("Entering Nuxeo Authentication Filter"); 445 } 446 447 String targetPageURL = null; 448 HttpServletRequest httpRequest = (HttpServletRequest) request; 449 HttpServletResponse httpResponse = (HttpServletResponse) response; 450 Principal principal = httpRequest.getUserPrincipal(); 451 452 NuxeoAuthenticationPropagator.CleanupCallback propagatedAuthCb = null; 453 454 String forceAnonymousLoginParam = httpRequest.getParameter(FORCE_ANONYMOUS_LOGIN); 455 boolean forceAnonymousLogin = Boolean.parseBoolean(forceAnonymousLoginParam); 456 457 try { 458 if (principal == null) { 459 log.debug("Principal not found inside Request via getUserPrincipal"); 460 // need to authenticate ! 461 462 // retrieve user & password 463 CachableUserIdentificationInfo cachableUserIdent; 464 if (avoidReauthenticate) { 465 log.debug("Try getting authentication from cache"); 466 cachableUserIdent = retrieveIdentityFromCache(httpRequest); 467 } else { 468 log.debug("Principal cache is NOT activated"); 469 } 470 471 if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null) { 472 if (cachableUserIdent.getUserInfo().getUserName().equals(getAnonymousId())) { 473 if (forceAnonymousLogin) { 474 cachableUserIdent = null; 475 } 476 } 477 478 if (service.needResetLogin(request)) { 479 HttpSession session = httpRequest.getSession(false); 480 if (session != null) { 481 session.removeAttribute(USERIDENT_KEY); 482 } 483 // first propagate the login because invalidation may 484 // require 485 // an authenticated session 486 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 487 // invalidate Session ! 488 try { 489 service.invalidateSession(request); 490 } finally { 491 if (propagatedAuthCb != null) { 492 propagatedAuthCb.cleanup(); 493 propagatedAuthCb = null; 494 } 495 } 496 // TODO perform logout? 497 cachableUserIdent = null; 498 } 499 } 500 501 // identity found in cache 502 if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null) { 503 log.debug("userIdent found in cache, get the Principal from it without reloggin"); 504 505 NuxeoHttpSessionMonitor.instance().updateEntry(httpRequest); 506 507 principal = cachableUserIdent.getPrincipal(); 508 log.debug("Principal = " + principal.getName()); 509 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 510 511 String requestedPage = getRequestedPage(httpRequest); 512 if (LOGOUT_PAGE.equals(requestedPage)) { 513 boolean redirected = handleLogout(request, response, cachableUserIdent); 514 cachableUserIdent = null; 515 principal = null; 516 if (redirected && httpRequest.getParameter(FORM_SUBMITTED_MARKER) == null) { 517 return; 518 } 519 } else if (LOGIN_PAGE.equals(requestedPage)) { 520 if (handleLogin(httpRequest, httpResponse)) { 521 return; 522 } 523 } else { 524 targetPageURL = getSavedRequestedURL(httpRequest, httpResponse); 525 } 526 } 527 528 // identity not found in cache or reseted by logout 529 if (cachableUserIdent == null || cachableUserIdent.getUserInfo() == null) { 530 UserIdentificationInfo userIdent = handleRetrieveIdentity(httpRequest, httpResponse); 531 if (userIdent != null && userIdent.containsValidIdentity() 532 && userIdent.getUserName().equals(getAnonymousId())) { 533 if (forceAnonymousLogin) { 534 userIdent = null; 535 } 536 } 537 if ((userIdent == null || !userIdent.containsValidIdentity()) && !bypassAuth(httpRequest)) { 538 boolean res = handleLoginPrompt(httpRequest, httpResponse); 539 if (res) { 540 return; 541 } 542 } else { 543 String redirectUrl = VirtualHostHelper.getRedirectUrl(httpRequest); 544 HttpSession session = httpRequest.getSession(false); 545 if (session != null) { 546 session.setAttribute(REDIRECT_URL, redirectUrl); 547 } 548 // restore saved Starting page 549 targetPageURL = getSavedRequestedURL(httpRequest, httpResponse); 550 } 551 if (userIdent != null && userIdent.containsValidIdentity()) { 552 // do the authentication 553 cachableUserIdent = new CachableUserIdentificationInfo(userIdent); 554 principal = doAuthenticate(cachableUserIdent, httpRequest); 555 if (principal != null && principal != DIRECTORY_ERROR_PRINCIPAL) { 556 // Do the propagation too ???? 557 propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent); 558 // setPrincipalToSession(httpRequest, principal); 559 // check if the current authenticator is a 560 // LoginResponseHandler 561 NuxeoAuthenticationPlugin plugin = getAuthenticator(cachableUserIdent); 562 if (plugin instanceof LoginResponseHandler) { 563 // call the extended error handler 564 if (((LoginResponseHandler) plugin).onSuccess((HttpServletRequest) request, 565 (HttpServletResponse) response)) { 566 return; 567 } 568 } 569 } else { 570 // first 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).onError((HttpServletRequest) request, 576 (HttpServletResponse) response)) { 577 return; 578 } 579 } else { 580 // use the old method 581 String err = principal == DIRECTORY_ERROR_PRINCIPAL ? ERROR_CONNECTION_FAILED 582 : ERROR_AUTHENTICATION_FAILED; 583 httpRequest.setAttribute(LOGIN_ERROR, err); 584 boolean res = handleLoginPrompt(httpRequest, httpResponse); 585 if (res) { 586 return; 587 } 588 } 589 } 590 591 } 592 } 593 } 594 595 if (principal != null) { 596 if (targetPageURL != null && targetPageURL.length() > 0) { 597 // forward to target page 598 String baseURL = service.getBaseURL(request); 599 600 // httpRequest.getRequestDispatcher(targetPageURL).forward(new 601 // NuxeoSecuredRequestWrapper(httpRequest, principal), 602 // response); 603 if (XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) { 604 // httpResponse.setStatus(200); 605 return; 606 } else { 607 // In case of a download redirection, the base url is already contained in the target 608 String url = targetPageURL.startsWith(baseURL) ? targetPageURL : baseURL + targetPageURL; 609 httpResponse.sendRedirect(url); 610 return; 611 } 612 613 } else { 614 // simply continue request 615 chain.doFilter(new NuxeoSecuredRequestWrapper(httpRequest, principal), response); 616 } 617 } else { 618 chain.doFilter(request, response); 619 } 620 } finally { 621 if (propagatedAuthCb != null) { 622 propagatedAuthCb.cleanup(); 623 } 624 } 625 if (!avoidReauthenticate) { 626 // destroy login context 627 log.debug("Log out"); 628 LoginContext lc = (LoginContext) httpRequest.getAttribute("LoginContext"); 629 if (lc != null) { 630 try { 631 lc.logout(); 632 } catch (LoginException e) { 633 log.error(e, e); 634 } 635 } 636 } 637 log.debug("Exit Nuxeo Authentication filter"); 638 } 639 640 public NuxeoAuthenticationPlugin getAuthenticator(CachableUserIdentificationInfo ci) { 641 String key = ci.getUserInfo().getAuthPluginName(); 642 if (key != null) { 643 NuxeoAuthenticationPlugin authPlugin = service.getPlugin(key); 644 return authPlugin; 645 } 646 return null; 647 } 648 649 protected static CachableUserIdentificationInfo retrieveIdentityFromCache(HttpServletRequest httpRequest) { 650 651 HttpSession session = httpRequest.getSession(false); 652 if (session != null) { 653 CachableUserIdentificationInfo cachableUserInfo = (CachableUserIdentificationInfo) session.getAttribute( 654 USERIDENT_KEY); 655 if (cachableUserInfo != null) { 656 return cachableUserInfo; 657 } 658 } 659 660 return null; 661 } 662 663 private String getAnonymousId() throws ServletException { 664 if (anonymous == null) { 665 anonymous = Framework.getService(UserManager.class).getAnonymousUserId(); 666 } 667 return anonymous; 668 } 669 670 protected void doInitIfNeeded() throws ServletException { 671 if (service == null && Framework.getRuntime() != null) { 672 synchronized (this) { 673 if (service != null) { 674 return; 675 } 676 service = (PluggableAuthenticationService) Framework.getRuntime() 677 .getComponent(PluggableAuthenticationService.NAME); 678 // init preFilters 679 service.initPreFilters(); 680 if (service == null) { 681 log.error("Unable to get Service " + PluggableAuthenticationService.NAME); 682 throw new ServletException("Can't initialize Nuxeo Pluggable Authentication Service"); 683 } else { 684 new ComponentManager.Listener() { 685 // nullify service field if components are restarting 686 @Override 687 public void beforeStart(ComponentManager mgr, boolean isResume) { 688 service = null; 689 uninstall(); 690 } 691 }.install(); 692 } 693 } 694 } 695 } 696 697 @Override 698 public void init(FilterConfig config) throws ServletException { 699 String val = config.getInitParameter("byPassAuthenticationLog"); 700 if (val != null && Boolean.parseBoolean(val)) { 701 byPassAuthenticationLog = true; 702 } 703 val = config.getInitParameter("securityDomain"); 704 if (val != null) { 705 securityDomain = val; 706 } 707 708 } 709 710 /** 711 * Save requested URL before redirecting to login form. 712 * <p> 713 * Returns true if target url is a valid startup page. 714 */ 715 public boolean saveRequestedURLBeforeRedirect(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 716 717 final boolean hasRequestedSessionId = !StringUtils.isBlank(httpRequest.getRequestedSessionId()); 718 719 HttpSession session = httpRequest.getSession(false); 720 final boolean isTimeout = session == null && hasRequestedSessionId; 721 722 if (!httpResponse.isCommitted()) { 723 session = httpRequest.getSession(true); 724 } 725 726 if (session == null) { 727 return false; 728 } 729 730 String requestPage; 731 boolean requestPageInParams = false; 732 if (httpRequest.getParameter(REQUESTED_URL) != null) { 733 requestPageInParams = true; 734 requestPage = httpRequest.getParameter(REQUESTED_URL); 735 } else { 736 requestPage = getRequestedUrl(httpRequest); 737 } 738 739 if (requestPage == null) { 740 return false; 741 } 742 743 // add a flag to tell that the Session looks like having timed out 744 if (isTimeout && !requestPage.equals(LoginScreenHelper.getStartupPagePath())) { 745 session.setAttribute(SESSION_TIMEOUT, Boolean.TRUE); 746 } else { 747 session.removeAttribute(SESSION_TIMEOUT); 748 } 749 750 // avoid saving to session is start page is not valid or if it's 751 // already in the request params 752 if (isStartPageValid(requestPage)) { 753 if (!requestPageInParams) { 754 session.setAttribute(START_PAGE_SAVE_KEY, requestPage); 755 } 756 return true; 757 } 758 759 // avoid redirect if not useful 760 for (String startupPagePath : LoginScreenHelper.getStartupPagePaths()) { 761 if (requestPage.startsWith(startupPagePath) 762 && LoginScreenHelper.getStartupPagePath().equals(startupPagePath)) { 763 return true; 764 } 765 } 766 767 return false; 768 } 769 770 public static String getRequestedUrl(HttpServletRequest httpRequest) { 771 String completeURI = httpRequest.getRequestURI(); 772 String qs = httpRequest.getQueryString(); 773 String context = httpRequest.getContextPath() + '/'; 774 String requestPage = completeURI.substring(context.length()); 775 if (qs != null && qs.length() > 0) { 776 // remove conversationId if present 777 if (qs.contains("conversationId")) { 778 qs = qs.replace("conversationId", "old_conversationId"); 779 } 780 requestPage = requestPage + '?' + qs; 781 } 782 return requestPage; 783 } 784 785 protected static String getSavedRequestedURL(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 786 787 HttpSession session = httpRequest.getSession(false); 788 String requestedPage = httpRequest.getParameter(REQUESTED_URL); 789 if (StringUtils.isBlank(requestedPage)) { 790 // retrieve from session 791 if (session != null) { 792 requestedPage = (String) session.getAttribute(START_PAGE_SAVE_KEY); 793 } 794 795 // retrieve from SSO cookies 796 Cookie[] cookies = httpRequest.getCookies(); 797 if (cookies != null) { 798 for (Cookie cookie : cookies) { 799 if (SSO_INITIAL_URL_REQUEST_KEY.equals(cookie.getName())) { 800 requestedPage = cookie.getValue(); 801 cookie.setPath("/"); 802 // enforce cookie removal 803 cookie.setMaxAge(0); 804 httpResponse.addCookie(cookie); 805 } 806 } 807 } 808 } 809 810 if (requestedPage != null) { 811 // retrieve URL fragment from cookie 812 Cookie[] cookies = httpRequest.getCookies(); 813 if (cookies != null) { 814 for (Cookie cookie : cookies) { 815 if (NXAuthConstants.START_PAGE_FRAGMENT_KEY.equals(cookie.getName())) { 816 try { 817 requestedPage = UriBuilder.fromUri(requestedPage) 818 .fragment(URLDecoder.decode(cookie.getValue(), "UTF-8")) 819 .build() 820 .toString(); 821 } catch (UnsupportedEncodingException e) { 822 log.error("Failed to decode start page url fragment", e); 823 } 824 // enforce cookie removal 825 cookie.setMaxAge(0); 826 httpResponse.addCookie(cookie); 827 } 828 } 829 } 830 } 831 832 // clean up session 833 if (session != null) { 834 session.removeAttribute(START_PAGE_SAVE_KEY); 835 } 836 837 return requestedPage; 838 } 839 840 protected boolean isStartPageValid(String startPage) { 841 if (startPage == null) { 842 return false; 843 } 844 try { 845 // Sometimes, the service is not initialized at startup 846 doInitIfNeeded(); 847 } catch (ServletException e) { 848 return false; 849 } 850 for (String prefix : service.getStartURLPatterns()) { 851 if (startPage.startsWith(prefix)) { 852 return true; 853 } 854 } 855 return false; 856 } 857 858 protected boolean handleLogout(ServletRequest request, ServletResponse response, 859 CachableUserIdentificationInfo cachedUserInfo) throws ServletException { 860 logLogout(cachedUserInfo.getUserInfo()); 861 862 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, Boolean.TRUE); 863 Map<String, String> parameters = new HashMap<>(); 864 String securityError = request.getParameter(SECURITY_ERROR); 865 if (securityError != null) { 866 parameters.put(SECURITY_ERROR, securityError); 867 } 868 if (cachedUserInfo.getPrincipal().getName().equals(getAnonymousId())) { 869 parameters.put(FORCE_ANONYMOUS_LOGIN, "true"); 870 } 871 String requestedUrl = request.getParameter(REQUESTED_URL); 872 if (requestedUrl != null) { 873 parameters.put(REQUESTED_URL, requestedUrl); 874 } 875 // Reset JSESSIONID Cookie 876 HttpServletResponse httpResponse = (HttpServletResponse) response; 877 Cookie cookie = new Cookie("JSESSIONID", null); 878 cookie.setMaxAge(0); 879 cookie.setPath("/"); 880 httpResponse.addCookie(cookie); 881 882 String pluginName = cachedUserInfo.getUserInfo().getAuthPluginName(); 883 NuxeoAuthenticationPlugin authPlugin = service.getPlugin(pluginName); 884 NuxeoAuthenticationPluginLogoutExtension logoutPlugin = null; 885 886 if (authPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) { 887 logoutPlugin = (NuxeoAuthenticationPluginLogoutExtension) authPlugin; 888 } 889 890 boolean redirected = false; 891 if (logoutPlugin != null) { 892 redirected = Boolean.TRUE.equals( 893 logoutPlugin.handleLogout((HttpServletRequest) request, (HttpServletResponse) response)); 894 } 895 896 // invalidate Session ! 897 service.invalidateSession(request); 898 899 HttpServletRequest httpRequest = (HttpServletRequest) request; 900 if (!redirected && !XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) { 901 String baseURL = service.getBaseURL(request); 902 try { 903 String url = baseURL + LoginScreenHelper.getStartupPagePath(); 904 url = URIUtils.addParametersToURIQuery(url, parameters); 905 ((HttpServletResponse) response).sendRedirect(url); 906 redirected = true; 907 } catch (IOException e) { 908 log.error("Unable to redirect to default start page after logout : " + e.getMessage()); 909 } 910 } 911 912 try { 913 cachedUserInfo.getLoginContext().logout(); 914 } catch (LoginException e) { 915 log.error("Unable to logout " + e.getMessage()); 916 } 917 return redirected; 918 } 919 920 // App Server JAAS SPI 921 protected void propagateUserIdentificationInformation(CachableUserIdentificationInfo cachableUserIdent) { 922 service.propagateUserIdentificationInformation(cachableUserIdent); 923 } 924 925 // Plugin API 926 protected void initUnAuthenticatedURLPrefix() { 927 // gather unAuthenticated URLs 928 unAuthenticatedURLPrefix = new ArrayList<>(); 929 for (String pluginName : service.getAuthChain()) { 930 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 931 List<String> prefix = plugin.getUnAuthenticatedURLPrefix(); 932 if (prefix != null && !prefix.isEmpty()) { 933 unAuthenticatedURLPrefix.addAll(prefix); 934 } 935 } 936 } 937 938 protected boolean bypassAuth(HttpServletRequest httpRequest) { 939 if (unAuthenticatedURLPrefix == null) { 940 try { 941 unAuthenticatedURLPrefixLock.writeLock().lock(); 942 // late init to allow plugins registered after this filter init 943 initUnAuthenticatedURLPrefix(); 944 } finally { 945 unAuthenticatedURLPrefixLock.writeLock().unlock(); 946 } 947 } 948 949 try { 950 unAuthenticatedURLPrefixLock.readLock().lock(); 951 String requestPage = getRequestedPage(httpRequest); 952 for (String prefix : unAuthenticatedURLPrefix) { 953 if (requestPage.startsWith(prefix)) { 954 return true; 955 } 956 } 957 } finally { 958 unAuthenticatedURLPrefixLock.readLock().unlock(); 959 } 960 961 List<OpenUrlDescriptor> openUrls = service.getOpenUrls(); 962 for (OpenUrlDescriptor openUrl : openUrls) { 963 if (openUrl.allowByPassAuth(httpRequest)) { 964 return true; 965 } 966 } 967 968 return false; 969 } 970 971 public static String getRequestedPage(ServletRequest request) { 972 if (request instanceof HttpServletRequest) { 973 HttpServletRequest httpRequest = (HttpServletRequest) request; 974 return getRequestedPage(httpRequest); 975 } else { 976 return null; 977 } 978 } 979 980 protected static String getRequestedPage(HttpServletRequest httpRequest) { 981 String requestURI = httpRequest.getRequestURI(); 982 String context = httpRequest.getContextPath() + '/'; 983 String requestedPage = requestURI.substring(context.length()); 984 int i = requestedPage.indexOf(';'); 985 return i == -1 ? requestedPage : requestedPage.substring(0, i); 986 } 987 988 protected boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 989 990 // A specific auth chain may prevent the filter to relay to a login prompt. 991 if (!service.doHandlePrompt(httpRequest)) { 992 buildUnauthorizedResponse(httpRequest, httpResponse); 993 return true; 994 } 995 996 return handleLogin(httpRequest, httpResponse); 997 998 } 999 1000 private boolean handleLogin(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { 1001 String baseURL = service.getBaseURL(httpRequest); 1002 1003 // go through plugins to get UserIndentity 1004 for (String pluginName : service.getAuthChain(httpRequest)) { 1005 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 1006 AuthenticationPluginDescriptor descriptor = service.getDescriptor(pluginName); 1007 1008 if (Boolean.TRUE.equals(plugin.needLoginPrompt(httpRequest))) { 1009 if (descriptor.getNeedStartingURLSaving()) { 1010 saveRequestedURLBeforeRedirect(httpRequest, httpResponse); 1011 } 1012 1013 HttpServletResponse response = new HttpServletResponseWrapper(httpResponse) { 1014 @Override 1015 public void sendRedirect(String location) throws IOException { 1016 HttpServletResponse response = (HttpServletResponse) getResponse(); 1017 StringBuilder sb = new StringBuilder(); 1018 sb.append("<script type=\"text/javascript\">\n"); 1019 sb.append("document.cookie = '" + NXAuthConstants.START_PAGE_FRAGMENT_KEY 1020 + "=' + encodeURIComponent(window.location.hash.substring(1) || '') + '; path=/';\n"); 1021 sb.append("window.location = '" + location + "';\n"); 1022 sb.append("</script>"); 1023 String script = sb.toString(); 1024 1025 response.setStatus(SC_UNAUTHORIZED); 1026 response.setContentType("text/html;charset=UTF-8"); 1027 response.setContentLength(script.length()); 1028 response.getWriter().write(script); 1029 } 1030 }; 1031 return Boolean.TRUE.equals(plugin.handleLoginPrompt(httpRequest, response, baseURL)); 1032 } 1033 } 1034 1035 log.warn("No auth plugin can be found to do the Login Prompt"); 1036 return false; 1037 } 1038 1039 private void buildUnauthorizedResponse(HttpServletRequest req, HttpServletResponse resp) { 1040 1041 try { 1042 String loginUrl = VirtualHostHelper.getBaseURL(req) + LOGIN_PAGE; 1043 resp.addHeader("Location", loginUrl); 1044 resp.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); 1045 resp.getWriter().write("Please log in at: " + loginUrl); 1046 } catch (IOException e) { 1047 log.error("Unable to write login page on unauthorized response", e); 1048 } 1049 } 1050 1051 protected UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, 1052 HttpServletResponse httpResponse) { 1053 1054 UserIdentificationInfo userIdent = null; 1055 1056 // go through plugins to get UserIdentity 1057 for (String pluginName : service.getAuthChain(httpRequest)) { 1058 NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName); 1059 if (plugin != null) { 1060 log.debug("Trying to retrieve userIdentification using plugin " + pluginName); 1061 userIdent = plugin.handleRetrieveIdentity(httpRequest, httpResponse); 1062 if (userIdent != null && userIdent.containsValidIdentity()) { 1063 // fill information for the Login module 1064 userIdent.setAuthPluginName(pluginName); 1065 1066 // get the target login module 1067 String loginModulePlugin = service.getDescriptor(pluginName).getLoginModulePlugin(); 1068 userIdent.setLoginPluginName(loginModulePlugin); 1069 1070 // get the additional parameters 1071 Map<String, String> parameters = service.getDescriptor(pluginName).getParameters(); 1072 if (userIdent.getLoginParameters() != null) { 1073 // keep existing parameters set by the auth plugin 1074 if (parameters == null) { 1075 parameters = new HashMap<>(); 1076 } 1077 parameters.putAll(userIdent.getLoginParameters()); 1078 } 1079 userIdent.setLoginParameters(parameters); 1080 1081 break; 1082 } 1083 } else { 1084 log.error("Auth plugin " + pluginName + " can not be retrieved from service"); 1085 } 1086 } 1087 1088 // Fall back to cache (used only when avoidReautenticated=false) 1089 if (userIdent == null || !userIdent.containsValidIdentity()) { 1090 log.debug("user/password not found in request, try into identity cache"); 1091 HttpSession session = httpRequest.getSession(false); 1092 if (session == null) { 1093 // possible we need a new session 1094 if (httpRequest.isRequestedSessionIdValid()) { 1095 session = httpRequest.getSession(true); 1096 } 1097 } 1098 if (session != null) { 1099 CachableUserIdentificationInfo savedUserInfo = retrieveIdentityFromCache(httpRequest); 1100 if (savedUserInfo != null) { 1101 log.debug("Found User identity in cache :" + savedUserInfo.getUserInfo().getUserName()); 1102 userIdent = new UserIdentificationInfo(savedUserInfo.getUserInfo()); 1103 savedUserInfo.setPrincipal(null); 1104 } 1105 } 1106 } else { 1107 log.debug("User/Password found as parameter of the request"); 1108 } 1109 1110 return userIdent; 1111 } 1112 1113 protected boolean needSessionSaving(UserIdentificationInfo userInfo) { 1114 String pluginName = userInfo.getAuthPluginName(); 1115 1116 AuthenticationPluginDescriptor desc = service.getDescriptor(pluginName); 1117 1118 if (desc.getStateful()) { 1119 return true; 1120 } else { 1121 return desc.getNeedStartingURLSaving(); 1122 } 1123 } 1124 1125 /** 1126 * Does a forced login as the given user. Bypasses all authentication checks. 1127 * 1128 * @param username the user name 1129 * @return the login context, which MUST be used for logout in a {@code finally} block 1130 * @throws LoginException 1131 */ 1132 public static LoginContext loginAs(String username) throws LoginException { 1133 UserIdentificationInfo userIdent = new UserIdentificationInfo(username, ""); 1134 userIdent.setLoginPluginName(TrustingLoginPlugin.NAME); 1135 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 1136 .getComponent( 1137 PluggableAuthenticationService.NAME); 1138 CallbackHandler callbackHandler; 1139 if (authService != null) { 1140 callbackHandler = authService.getCallbackHandler(userIdent); 1141 } else { 1142 callbackHandler = new UserIdentificationInfoCallbackHandler(userIdent); 1143 } 1144 LoginContext loginContext = new LoginContext(LOGIN_DOMAIN, callbackHandler); 1145 1146 if (isLoginSynchronized()) { 1147 synchronized (NuxeoAuthenticationFilter.class) { 1148 loginContext.login(); 1149 } 1150 } else { 1151 loginContext.login(); 1152 } 1153 return loginContext; 1154 } 1155 1156}