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