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