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