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