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