001/*
002 * (C) Copyright 2006-2017 Nuxeo (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 */
022package org.nuxeo.ecm.platform.ui.web.auth;
023
024import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY;
025import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.ERROR_AUTHENTICATION_FAILED;
026import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.ERROR_CONNECTION_FAILED;
027import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORCE_ANONYMOUS_LOGIN;
028import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORM_SUBMITTED_MARKER;
029import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGINCONTEXT_KEY;
030import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_ERROR;
031import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_PAGE;
032import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_STATUS_CODE;
033import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGOUT_PAGE;
034import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.PAGE_AFTER_SWITCH;
035import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REDIRECT_URL;
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.metrics.MetricsService;
103import org.nuxeo.runtime.model.ComponentManager;
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<>();
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            contextTimer.stop();
413            concurrentCount.dec();
414        }
415    }
416
417    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
418            throws IOException, ServletException {
419
420        if (bypassAuth((HttpServletRequest) request)) {
421            chain.doFilter(request, response);
422            return;
423        }
424
425        String tokenPage = getRequestedPage(request);
426        if (tokenPage.equals(SWITCH_USER_PAGE)) {
427            boolean result = switchUser(request, response, chain);
428            if (result) {
429                return;
430            }
431        }
432
433        if (request instanceof NuxeoSecuredRequestWrapper) {
434            log.debug("ReEntering Nuxeo Authentication Filter ... exiting directly");
435            chain.doFilter(request, response);
436            return;
437        } else if (service.canBypassRequest(request)) {
438            log.debug("ReEntering Nuxeo Authentication Filter after URL rewrite ... exiting directly");
439            chain.doFilter(request, response);
440            return;
441        } else {
442            log.debug("Entering Nuxeo Authentication Filter");
443        }
444
445        String targetPageURL = null;
446        HttpServletRequest httpRequest = (HttpServletRequest) request;
447        HttpServletResponse httpResponse = (HttpServletResponse) response;
448        Principal principal = httpRequest.getUserPrincipal();
449
450        NuxeoAuthenticationPropagator.CleanupCallback propagatedAuthCb = null;
451
452        try {
453            if (principal == null) {
454                log.debug("Principal not found inside Request via getUserPrincipal");
455                // need to authenticate !
456
457                // retrieve user & password
458                CachableUserIdentificationInfo cachableUserIdent;
459                if (avoidReauthenticate) {
460                    log.debug("Try getting authentication from cache");
461                    cachableUserIdent = retrieveIdentityFromCache(httpRequest);
462                } else {
463                    log.debug("Principal cache is NOT activated");
464                }
465
466                if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null
467                        && service.needResetLogin(request)) {
468                    HttpSession session = httpRequest.getSession(false);
469                    if (session != null) {
470                        session.removeAttribute(USERIDENT_KEY);
471                    }
472                    // first propagate the login because invalidation may
473                    // require
474                    // an authenticated session
475                    propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent);
476                    // invalidate Session !
477                    try {
478                        service.invalidateSession(request);
479                    } finally {
480                        if (propagatedAuthCb != null) {
481                            propagatedAuthCb.cleanup();
482                            propagatedAuthCb = null;
483                        }
484                    }
485                    // TODO perform logout?
486                    cachableUserIdent = null;
487                }
488
489                // identity found in cache
490                if (cachableUserIdent != null && cachableUserIdent.getUserInfo() != null) {
491                    log.debug("userIdent found in cache, get the Principal from it without reloggin");
492
493                    NuxeoHttpSessionMonitor.instance().updateEntry(httpRequest);
494
495                    principal = cachableUserIdent.getPrincipal();
496                    log.debug("Principal = " + principal.getName());
497                    propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent);
498
499                    String requestedPage = getRequestedPage(httpRequest);
500                    if (LOGOUT_PAGE.equals(requestedPage)) {
501                        boolean redirected = handleLogout(request, response, cachableUserIdent);
502                        cachableUserIdent = null;
503                        principal = null;
504                        if (redirected && httpRequest.getParameter(FORM_SUBMITTED_MARKER) == null) {
505                            return;
506                        }
507                    } else if (LOGIN_PAGE.equals(requestedPage)) {
508                        if (handleLogin(httpRequest, httpResponse)) {
509                            return;
510                        }
511                    } else {
512                        targetPageURL = getSavedRequestedURL(httpRequest, httpResponse);
513                    }
514                }
515
516                // identity not found in cache or reseted by logout
517                if (cachableUserIdent == null || cachableUserIdent.getUserInfo() == null) {
518                    UserIdentificationInfo userIdent = handleRetrieveIdentity(httpRequest, httpResponse);
519                    if (userIdent != null && userIdent.containsValidIdentity()
520                            && userIdent.getUserName().equals(getAnonymousId())) {
521                        String forceAuth = httpRequest.getParameter(FORCE_ANONYMOUS_LOGIN);
522                        if (forceAuth != null && forceAuth.equals("true")) {
523                            userIdent = null;
524                        }
525                    }
526                    if ((userIdent == null || !userIdent.containsValidIdentity()) && !bypassAuth(httpRequest)) {
527                        boolean res = handleLoginPrompt(httpRequest, httpResponse);
528                        if (res) {
529                            return;
530                        }
531                    } else {
532                        String redirectUrl = VirtualHostHelper.getRedirectUrl(httpRequest);
533                        HttpSession session = httpRequest.getSession(false);
534                        if (session != null) {
535                            session.setAttribute(REDIRECT_URL, redirectUrl);
536                        }
537                        // restore saved Starting page
538                        targetPageURL = getSavedRequestedURL(httpRequest, httpResponse);
539                    }
540                    if (userIdent != null && userIdent.containsValidIdentity()) {
541                        // do the authentication
542                        cachableUserIdent = new CachableUserIdentificationInfo(userIdent);
543                        principal = doAuthenticate(cachableUserIdent, httpRequest);
544                        if (principal != null && principal != DIRECTORY_ERROR_PRINCIPAL) {
545                            // Do the propagation too ????
546                            propagatedAuthCb = service.propagateUserIdentificationInformation(cachableUserIdent);
547                            // setPrincipalToSession(httpRequest, principal);
548                            // check if the current authenticator is a
549                            // LoginResponseHandler
550                            NuxeoAuthenticationPlugin plugin = getAuthenticator(cachableUserIdent);
551                            if (plugin instanceof LoginResponseHandler) {
552                                // call the extended error handler
553                                if (((LoginResponseHandler) plugin).onSuccess((HttpServletRequest) request,
554                                        (HttpServletResponse) response)) {
555                                    return;
556                                }
557                            }
558                        } else {
559                            // first check if the current authenticator is a
560                            // LoginResponseHandler
561                            NuxeoAuthenticationPlugin plugin = getAuthenticator(cachableUserIdent);
562                            if (plugin instanceof LoginResponseHandler) {
563                                // call the extended error handler
564                                if (((LoginResponseHandler) plugin).onError((HttpServletRequest) request,
565                                        (HttpServletResponse) response)) {
566                                    return;
567                                }
568                            } else {
569                                // use the old method
570                                String err = principal == DIRECTORY_ERROR_PRINCIPAL ? ERROR_CONNECTION_FAILED
571                                        : ERROR_AUTHENTICATION_FAILED;
572                                httpRequest.setAttribute(LOGIN_ERROR, err);
573                                boolean res = handleLoginPrompt(httpRequest, httpResponse);
574                                if (res) {
575                                    return;
576                                }
577                            }
578                        }
579
580                    }
581                }
582            }
583
584            if (principal != null) {
585                if (targetPageURL != null && targetPageURL.length() > 0) {
586                    // forward to target page
587                    String baseURL = service.getBaseURL(request);
588
589                    // httpRequest.getRequestDispatcher(targetPageURL).forward(new
590                    // NuxeoSecuredRequestWrapper(httpRequest, principal),
591                    // response);
592                    if (XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) {
593                        // httpResponse.setStatus(200);
594                        return;
595                    } else {
596                        // In case of a download redirection, the base url is already contained in the target
597                        String url = targetPageURL.startsWith(baseURL) ? targetPageURL : baseURL + targetPageURL;
598                        httpResponse.sendRedirect(url);
599                        return;
600                    }
601
602                } else {
603                    // simply continue request
604                    chain.doFilter(new NuxeoSecuredRequestWrapper(httpRequest, principal), response);
605                }
606            } else {
607                chain.doFilter(request, response);
608            }
609        } finally {
610            if (propagatedAuthCb != null) {
611                propagatedAuthCb.cleanup();
612            }
613        }
614        if (!avoidReauthenticate) {
615            // destroy login context
616            log.debug("Log out");
617            LoginContext lc = (LoginContext) httpRequest.getAttribute("LoginContext");
618            if (lc != null) {
619                try {
620                    lc.logout();
621                } catch (LoginException e) {
622                    log.error(e, e);
623                }
624            }
625        }
626        log.debug("Exit Nuxeo Authentication filter");
627    }
628
629    public NuxeoAuthenticationPlugin getAuthenticator(CachableUserIdentificationInfo ci) {
630        String key = ci.getUserInfo().getAuthPluginName();
631        if (key != null) {
632            NuxeoAuthenticationPlugin authPlugin = service.getPlugin(key);
633            return authPlugin;
634        }
635        return null;
636    }
637
638    protected static CachableUserIdentificationInfo retrieveIdentityFromCache(HttpServletRequest httpRequest) {
639
640        HttpSession session = httpRequest.getSession(false);
641        if (session != null) {
642            CachableUserIdentificationInfo cachableUserInfo = (CachableUserIdentificationInfo) session.getAttribute(
643                    USERIDENT_KEY);
644            if (cachableUserInfo != null) {
645                return cachableUserInfo;
646            }
647        }
648
649        return null;
650    }
651
652    private String getAnonymousId() throws ServletException {
653        if (anonymous == null) {
654            anonymous = Framework.getService(UserManager.class).getAnonymousUserId();
655        }
656        return anonymous;
657    }
658
659    protected void doInitIfNeeded() throws ServletException {
660        if (service == null && Framework.getRuntime() != null) {
661            synchronized (this) {
662                if (service != null) {
663                    return;
664                }
665                service = (PluggableAuthenticationService) Framework.getRuntime()
666                                                                    .getComponent(PluggableAuthenticationService.NAME);
667                // init preFilters
668                service.initPreFilters();
669                if (service == null) {
670                    log.error("Unable to get Service " + PluggableAuthenticationService.NAME);
671                    throw new ServletException("Can't initialize Nuxeo Pluggable Authentication Service");
672                } else {
673                    new ComponentManager.LifeCycleHandler() {
674                        // nullify service field if components are restarting
675                        @Override
676                        public void beforeStart(ComponentManager mgr, boolean isResume) {
677                            service = null;
678                            uninstall();
679                        }
680                    }.install();
681                }
682            }
683        }
684    }
685
686    @Override
687    public void init(FilterConfig config) throws ServletException {
688        String val = config.getInitParameter("byPassAuthenticationLog");
689        if (val != null && Boolean.parseBoolean(val)) {
690            byPassAuthenticationLog = true;
691        }
692        val = config.getInitParameter("securityDomain");
693        if (val != null) {
694            securityDomain = val;
695        }
696
697    }
698
699    /**
700     * Save requested URL before redirecting to login form.
701     * <p>
702     * Returns true if target url is a valid startup page.
703     */
704    public boolean saveRequestedURLBeforeRedirect(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
705
706        final boolean hasRequestedSessionId = !StringUtils.isBlank(httpRequest.getRequestedSessionId());
707
708        HttpSession session = httpRequest.getSession(false);
709        final boolean isTimeout = session == null && hasRequestedSessionId;
710
711        if (!httpResponse.isCommitted()) {
712            session = httpRequest.getSession(true);
713        }
714
715        if (session == null) {
716            return false;
717        }
718
719        String requestPage;
720        boolean requestPageInParams = false;
721        if (httpRequest.getParameter(REQUESTED_URL) != null) {
722            requestPageInParams = true;
723            requestPage = httpRequest.getParameter(REQUESTED_URL);
724        } else {
725            requestPage = getRequestedUrl(httpRequest);
726        }
727
728        if (requestPage == null) {
729            return false;
730        }
731
732        // add a flag to tell that the Session looks like having timed out
733        if (isTimeout && !requestPage.equals(LoginScreenHelper.getStartupPagePath())) {
734            session.setAttribute(SESSION_TIMEOUT, Boolean.TRUE);
735        } else {
736            session.removeAttribute(SESSION_TIMEOUT);
737        }
738
739        // avoid redirect if not useful
740        for (String startupPagePath : LoginScreenHelper.getStartupPagePaths()) {
741            if (requestPage.startsWith(startupPagePath)
742                    && LoginScreenHelper.getStartupPagePath().equals(startupPagePath)) {
743                return true;
744            }
745        }
746
747        // avoid saving to session is start page is not valid or if it's
748        // already in the request params
749        if (isStartPageValid(requestPage)) {
750            if (!requestPageInParams) {
751                session.setAttribute(START_PAGE_SAVE_KEY, requestPage);
752            }
753            return true;
754        }
755
756        return false;
757    }
758
759    public static String getRequestedUrl(HttpServletRequest httpRequest) {
760        String completeURI = httpRequest.getRequestURI();
761        String qs = httpRequest.getQueryString();
762        String context = httpRequest.getContextPath() + '/';
763        String requestPage = completeURI.substring(context.length());
764        if (qs != null && qs.length() > 0) {
765            // remove conversationId if present
766            if (qs.contains("conversationId")) {
767                qs = qs.replace("conversationId", "old_conversationId");
768            }
769            requestPage = requestPage + '?' + qs;
770        }
771        return requestPage;
772    }
773
774    protected static String getSavedRequestedURL(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
775
776        String requestedPage = null;
777        HttpSession session = httpRequest.getSession(false);
778        if (httpRequest.getParameter(REQUESTED_URL) != null) {
779            String requestedUrl = httpRequest.getParameter(REQUESTED_URL);
780            if (requestedUrl != null && !"".equals(requestedUrl)) {
781                try {
782                    requestedPage = URLDecoder.decode(requestedUrl, "UTF-8");
783                } catch (UnsupportedEncodingException e) {
784                    log.error("Unable to get the requestedUrl parameter" + e);
785                }
786            }
787        } else {
788            // retrieve from session
789            if (session != null) {
790                requestedPage = (String) session.getAttribute(START_PAGE_SAVE_KEY);
791            }
792
793            // retrieve from SSO cookies
794            Cookie[] cookies = httpRequest.getCookies();
795            if (cookies != null) {
796                for (Cookie cookie : cookies) {
797                    if (SSO_INITIAL_URL_REQUEST_KEY.equals(cookie.getName())) {
798                        requestedPage = cookie.getValue();
799                        cookie.setPath("/");
800                        // enforce cookie removal
801                        cookie.setMaxAge(0);
802                        httpResponse.addCookie(cookie);
803                    }
804                }
805            }
806        }
807
808        // clean up session
809        if (session != null) {
810            session.removeAttribute(START_PAGE_SAVE_KEY);
811        }
812
813        // add locale if not in the URL params
814        String localeStr = httpRequest.getParameter(NXAuthConstants.LANGUAGE_PARAMETER);
815        if (requestedPage != null && !"".equals(requestedPage) && localeStr != null) {
816            Map<String, String> params = new HashMap<>();
817            if (!URIUtils.getRequestParameters(requestedPage).containsKey(NXAuthConstants.LANGUAGE_PARAMETER)) {
818                params.put(NXAuthConstants.LANGUAGE_PARAMETER, localeStr);
819            }
820            return URIUtils.addParametersToURIQuery(requestedPage, params);
821        }
822
823        return requestedPage;
824    }
825
826    protected boolean isStartPageValid(String startPage) {
827        if (startPage == null) {
828            return false;
829        }
830        try {
831            // Sometimes, the service is not initialized at startup
832            doInitIfNeeded();
833        } catch (ServletException e) {
834            return false;
835        }
836        for (String prefix : service.getStartURLPatterns()) {
837            if (startPage.startsWith(prefix)) {
838                return true;
839            }
840        }
841        return false;
842    }
843
844    protected boolean handleLogout(ServletRequest request, ServletResponse response,
845            CachableUserIdentificationInfo cachedUserInfo) throws ServletException {
846        logLogout(cachedUserInfo.getUserInfo());
847
848        request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, Boolean.TRUE);
849        Map<String, String> parameters = new HashMap<>();
850        String securityError = request.getParameter(SECURITY_ERROR);
851        if (securityError != null) {
852            parameters.put(SECURITY_ERROR, securityError);
853        }
854        if (cachedUserInfo.getPrincipal().getName().equals(getAnonymousId())) {
855            parameters.put(FORCE_ANONYMOUS_LOGIN, "true");
856        }
857        String requestedUrl = request.getParameter(REQUESTED_URL);
858        if (requestedUrl != null) {
859            parameters.put(REQUESTED_URL, requestedUrl);
860        }
861        // Reset JSESSIONID Cookie
862        HttpServletResponse httpResponse = (HttpServletResponse) response;
863        Cookie cookie = new Cookie("JSESSIONID", null);
864        cookie.setMaxAge(0);
865        cookie.setPath("/");
866        httpResponse.addCookie(cookie);
867
868        String pluginName = cachedUserInfo.getUserInfo().getAuthPluginName();
869        NuxeoAuthenticationPlugin authPlugin = service.getPlugin(pluginName);
870        NuxeoAuthenticationPluginLogoutExtension logoutPlugin = null;
871
872        if (authPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) {
873            logoutPlugin = (NuxeoAuthenticationPluginLogoutExtension) authPlugin;
874        }
875
876        boolean redirected = false;
877        if (logoutPlugin != null) {
878            redirected = Boolean.TRUE.equals(
879                    logoutPlugin.handleLogout((HttpServletRequest) request, (HttpServletResponse) response));
880        }
881
882        // invalidate Session !
883        service.invalidateSession(request);
884
885        HttpServletRequest httpRequest = (HttpServletRequest) request;
886        if (!redirected && !XMLHTTP_REQUEST_TYPE.equalsIgnoreCase(httpRequest.getHeader("X-Requested-With"))) {
887            String baseURL = service.getBaseURL(request);
888            try {
889                String url = baseURL + LoginScreenHelper.getStartupPagePath();
890                url = URIUtils.addParametersToURIQuery(url, parameters);
891                ((HttpServletResponse) response).sendRedirect(url);
892                redirected = true;
893            } catch (IOException e) {
894                log.error("Unable to redirect to default start page after logout : " + e.getMessage());
895            }
896        }
897
898        try {
899            cachedUserInfo.getLoginContext().logout();
900        } catch (LoginException e) {
901            log.error("Unable to logout " + e.getMessage());
902        }
903        return redirected;
904    }
905
906    // App Server JAAS SPI
907    protected void propagateUserIdentificationInformation(CachableUserIdentificationInfo cachableUserIdent) {
908        service.propagateUserIdentificationInformation(cachableUserIdent);
909    }
910
911    // Plugin API
912    protected void initUnAuthenticatedURLPrefix() {
913        // gather unAuthenticated URLs
914        unAuthenticatedURLPrefix = new ArrayList<>();
915        for (String pluginName : service.getAuthChain()) {
916            NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName);
917            List<String> prefix = plugin.getUnAuthenticatedURLPrefix();
918            if (prefix != null && !prefix.isEmpty()) {
919                unAuthenticatedURLPrefix.addAll(prefix);
920            }
921        }
922    }
923
924    protected boolean bypassAuth(HttpServletRequest httpRequest) {
925        if (unAuthenticatedURLPrefix == null) {
926            try {
927                unAuthenticatedURLPrefixLock.writeLock().lock();
928                // late init to allow plugins registered after this filter init
929                initUnAuthenticatedURLPrefix();
930            } finally {
931                unAuthenticatedURLPrefixLock.writeLock().unlock();
932            }
933        }
934
935        try {
936            unAuthenticatedURLPrefixLock.readLock().lock();
937            String requestPage = getRequestedPage(httpRequest);
938            for (String prefix : unAuthenticatedURLPrefix) {
939                if (requestPage.startsWith(prefix)) {
940                    return true;
941                }
942            }
943        } finally {
944            unAuthenticatedURLPrefixLock.readLock().unlock();
945        }
946
947        List<OpenUrlDescriptor> openUrls = service.getOpenUrls();
948        for (OpenUrlDescriptor openUrl : openUrls) {
949            if (openUrl.allowByPassAuth(httpRequest)) {
950                return true;
951            }
952        }
953
954        return false;
955    }
956
957    public static String getRequestedPage(ServletRequest request) {
958        if (request instanceof HttpServletRequest) {
959            HttpServletRequest httpRequest = (HttpServletRequest) request;
960            return getRequestedPage(httpRequest);
961        } else {
962            return null;
963        }
964    }
965
966    protected static String getRequestedPage(HttpServletRequest httpRequest) {
967        String requestURI = httpRequest.getRequestURI();
968        String context = httpRequest.getContextPath() + '/';
969        String requestedPage = requestURI.substring(context.length());
970        int i = requestedPage.indexOf(';');
971        return i == -1 ? requestedPage : requestedPage.substring(0, i);
972    }
973
974    protected boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
975
976        // A specific auth chain may prevent the filter to relay to a login prompt.
977        if (!service.doHandlePrompt(httpRequest)) {
978            buildUnauthorizedResponse(httpRequest, httpResponse);
979            return true;
980        }
981
982        return handleLogin(httpRequest, httpResponse);
983
984    }
985
986    private boolean handleLogin(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
987        String baseURL = service.getBaseURL(httpRequest);
988
989        // go through plugins to get UserIndentity
990        for (String pluginName : service.getAuthChain(httpRequest)) {
991            NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName);
992            AuthenticationPluginDescriptor descriptor = service.getDescriptor(pluginName);
993
994            if (Boolean.TRUE.equals(plugin.needLoginPrompt(httpRequest))) {
995                if (descriptor.getNeedStartingURLSaving()) {
996                    saveRequestedURLBeforeRedirect(httpRequest, httpResponse);
997                }
998                return Boolean.TRUE.equals(plugin.handleLoginPrompt(httpRequest, httpResponse, baseURL));
999            }
1000        }
1001
1002        log.warn("No auth plugin can be found to do the Login Prompt");
1003        return false;
1004    }
1005
1006    private void buildUnauthorizedResponse(HttpServletRequest req, HttpServletResponse resp) {
1007
1008        try {
1009            String loginUrl = VirtualHostHelper.getBaseURL(req) + LOGIN_PAGE;
1010            resp.addHeader("Location", loginUrl);
1011            resp.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
1012            resp.getWriter().write("Please log in at: " + loginUrl);
1013        } catch (IOException e) {
1014            log.error("Unable to write login page on unauthorized response", e);
1015        }
1016    }
1017
1018    protected UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
1019            HttpServletResponse httpResponse) {
1020
1021        UserIdentificationInfo userIdent = null;
1022
1023        // go through plugins to get UserIdentity
1024        for (String pluginName : service.getAuthChain(httpRequest)) {
1025            NuxeoAuthenticationPlugin plugin = service.getPlugin(pluginName);
1026            if (plugin != null) {
1027                log.debug("Trying to retrieve userIdentification using plugin " + pluginName);
1028                userIdent = plugin.handleRetrieveIdentity(httpRequest, httpResponse);
1029                if (userIdent != null && userIdent.containsValidIdentity()) {
1030                    // fill information for the Login module
1031                    userIdent.setAuthPluginName(pluginName);
1032
1033                    // get the target login module
1034                    String loginModulePlugin = service.getDescriptor(pluginName).getLoginModulePlugin();
1035                    userIdent.setLoginPluginName(loginModulePlugin);
1036
1037                    // get the additional parameters
1038                    Map<String, String> parameters = service.getDescriptor(pluginName).getParameters();
1039                    if (userIdent.getLoginParameters() != null) {
1040                        // keep existing parameters set by the auth plugin
1041                        if (parameters == null) {
1042                            parameters = new HashMap<>();
1043                        }
1044                        parameters.putAll(userIdent.getLoginParameters());
1045                    }
1046                    userIdent.setLoginParameters(parameters);
1047
1048                    break;
1049                }
1050            } else {
1051                log.error("Auth plugin " + pluginName + " can not be retrieved from service");
1052            }
1053        }
1054
1055        // Fall back to cache (used only when avoidReautenticated=false)
1056        if (userIdent == null || !userIdent.containsValidIdentity()) {
1057            log.debug("user/password not found in request, try into identity cache");
1058            HttpSession session = httpRequest.getSession(false);
1059            if (session == null) {
1060                // possible we need a new session
1061                if (httpRequest.isRequestedSessionIdValid()) {
1062                    session = httpRequest.getSession(true);
1063                }
1064            }
1065            if (session != null) {
1066                CachableUserIdentificationInfo savedUserInfo = retrieveIdentityFromCache(httpRequest);
1067                if (savedUserInfo != null) {
1068                    log.debug("Found User identity in cache :" + savedUserInfo.getUserInfo().getUserName());
1069                    userIdent = new UserIdentificationInfo(savedUserInfo.getUserInfo());
1070                    savedUserInfo.setPrincipal(null);
1071                }
1072            }
1073        } else {
1074            log.debug("User/Password found as parameter of the request");
1075        }
1076
1077        return userIdent;
1078    }
1079
1080    protected boolean needSessionSaving(UserIdentificationInfo userInfo) {
1081        String pluginName = userInfo.getAuthPluginName();
1082
1083        AuthenticationPluginDescriptor desc = service.getDescriptor(pluginName);
1084
1085        if (desc.getStateful()) {
1086            return true;
1087        } else {
1088            return desc.getNeedStartingURLSaving();
1089        }
1090    }
1091
1092    /**
1093     * Does a forced login as the given user. Bypasses all authentication checks.
1094     *
1095     * @param username the user name
1096     * @return the login context, which MUST be used for logout in a {@code finally} block
1097     * @throws LoginException
1098     */
1099    public static LoginContext loginAs(String username) throws LoginException {
1100        UserIdentificationInfo userIdent = new UserIdentificationInfo(username, "");
1101        userIdent.setLoginPluginName(TrustingLoginPlugin.NAME);
1102        PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime()
1103                                                                                               .getComponent(
1104                                                                                                       PluggableAuthenticationService.NAME);
1105        CallbackHandler callbackHandler;
1106        if (authService != null) {
1107            callbackHandler = authService.getCallbackHandler(userIdent);
1108        } else {
1109            callbackHandler = new UserIdentificationInfoCallbackHandler(userIdent);
1110        }
1111        LoginContext loginContext = new LoginContext(LOGIN_DOMAIN, callbackHandler);
1112
1113        if (isLoginSynchronized()) {
1114            synchronized (NuxeoAuthenticationFilter.class) {
1115                loginContext.login();
1116            }
1117        } else {
1118            loginContext.login();
1119        }
1120        return loginContext;
1121    }
1122
1123}