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