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