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}