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