package com.psddev.cms.tool; import java.io.IOException; import java.util.UUID; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.psddev.cms.db.Preview; import com.psddev.cms.db.ToolUser; import com.psddev.dari.db.Database; import com.psddev.dari.db.ForwardingDatabase; import com.psddev.dari.db.Query; import com.psddev.dari.util.AbstractFilter; import com.psddev.dari.util.DebugFilter; import com.psddev.dari.util.JspUtils; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.PageContextFilter; import com.psddev.dari.util.Settings; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.UrlBuilder; public class AuthenticationFilter extends AbstractFilter { /** * Settings key for tool user session timeout (in milliseconds). */ public static final String TOOL_USER_SESSION_TIMEOUT_SETTING = "brightspot/toolUserSessionTimeout"; private static final String ATTRIBUTE_PREFIX = AuthenticationFilter.class.getName() + "."; public static final String AUTHENTICATED_ATTRIBUTE = ATTRIBUTE_PREFIX + "authenticated"; public static final String DATABASE_OVERRIDDEN_ATTRIBUTE = ATTRIBUTE_PREFIX + "databaseOverridden"; /** * @deprecated Don't use this directly. */ @Deprecated public static final String USER_ATTRIBUTE = ATTRIBUTE_PREFIX + "user"; public static final String USER_TOKEN = ATTRIBUTE_PREFIX + "token"; /** * @deprecated Don't use this directly. */ @Deprecated public static final String USER_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + "userChecked"; public static final String USER_SETTINGS_CHANGED_ATTRIBUTE = ATTRIBUTE_PREFIX + "userSettingsChanged"; private static final String INSECURE_TOOL_USER_ATTRIBUTE = ATTRIBUTE_PREFIX + "insecureToolUser"; private static final String INSECURE_TOOL_USER_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + "insecureToolUserChecked"; private static final String PREVIEW_ATTRIBUTE = ATTRIBUTE_PREFIX + "preview"; private static final String PREVIEW_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + "previewChecked"; private static final String TOOL_USER_ATTRIBUTE = ATTRIBUTE_PREFIX + "toolUser"; private static final String TOOL_USER_CHECKED_ATTRIBUTE = ATTRIBUTE_PREFIX + "toolUserChecked"; public static final String LOG_IN_PATH = "/logIn.jsp"; public static final String RETURN_PATH_PARAMETER = "returnPath"; /** * @deprecated Don't use this directly. */ @Deprecated public static final String USER_COOKIE = "cmsToolUser"; private static final String INSECURE_TOOL_USER_COOKIE = "bsp.itu"; private static final String PREVIEW_COOKIE = "bsp.p"; private static final String TOOL_USER_COOKIE = "bsp.tu"; // --- AbstractFilter support --- @Override protected void doRequest( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception { if (ObjectUtils.to(boolean.class, request.getParameter("_clearPreview"))) { Static.removeCurrentPreview(request, response); response.sendRedirect(new UrlBuilder(request) .currentPath() .currentParameters() .parameter("_clearPreview", null) .toString()); return; } try { chain.doFilter(request, response); } catch (Exception error) { if (Static.isAuthenticated(request)) { DebugFilter.Static.writeError(request, response, error); } else { throw error; } } finally { if (Boolean.TRUE.equals(request.getAttribute(DATABASE_OVERRIDDEN_ATTRIBUTE))) { Database.Static.setIgnoreReadConnection(false); Database.Static.restoreDefault(); } ToolUser user = Static.getUser(request); if (user != null && Boolean.TRUE.equals(request.getAttribute(USER_SETTINGS_CHANGED_ATTRIBUTE))) { user.save(); } } } /** * {@link AuthenticationFilter} utility methods. */ public static final class Static { private static String getEnvironmentName() { return Settings.getOrDefault(String.class, "cms/tool/environmentName", ""); } private static String getToolUserCookieName() { return TOOL_USER_COOKIE + getEnvironmentName(); } private static String getInsecureToolUserCookieName() { return INSECURE_TOOL_USER_COOKIE + getEnvironmentName(); } private static String getPreviewCookieName() { return PREVIEW_COOKIE + getEnvironmentName(); } private static void setSignedCookie( HttpServletRequest request, HttpServletResponse response, String name, String value, int maxAge, boolean secure) { Cookie c = new Cookie(name, name + value); c.setHttpOnly(true); c.setMaxAge(maxAge); c.setSecure(secure && JspUtils.isSecure(request)); String siteUrl = Query.from(CmsTool.class).first().getDefaultSiteUrl(); if (!ObjectUtils.isBlank(siteUrl)) { siteUrl = siteUrl.replaceFirst("^(?i)(?:https?://)?(?:www\\.)?", ""); int slashAt = siteUrl.indexOf('/'); if (slashAt > -1) { siteUrl = siteUrl.substring(0, slashAt); } int colonAt = siteUrl.indexOf(':'); if (colonAt > -1) { siteUrl = siteUrl.substring(0, colonAt); } c.setDomain(siteUrl); } c.setPath("/"); JspUtils.setSignedCookie(response, c); Cookie dc = new Cookie(name, name + value); dc.setHttpOnly(true); dc.setMaxAge(maxAge); dc.setSecure(secure && JspUtils.isSecure(request)); dc.setPath("/"); JspUtils.setSignedCookie(response, dc); } /** * Logs in the given tool {@code user} with the given {@code token}. * * @param request Can't be {@code null}. * @param response Can't be {@code null}. * @param user Can't be {@code null}. * @param token May be {@code null}. */ public static void logIn(HttpServletRequest request, HttpServletResponse response, ToolUser user, String token) { if (token == null) { logIn(request, response, user); } else { setSignedCookie(request, response, getToolUserCookieName(), token, -1, true); setSignedCookie(request, response, getInsecureToolUserCookieName(), token, -1, false); request.setAttribute(USER_ATTRIBUTE, user); request.setAttribute(USER_TOKEN, token); request.setAttribute(USER_CHECKED_ATTRIBUTE, Boolean.TRUE); } } /** * Logs in the given tool {@code user}. * * @param request Can't be {@code null}. * @param response Can't be {@code null}. * @param user Can't be {@code null}. */ public static void logIn(HttpServletRequest request, HttpServletResponse response, ToolUser user) { String token = (String) request.getAttribute(USER_TOKEN); if (token == null || (user != null && user.getId().toString().equals(token))) { token = user.generateLoginToken(); } else { boolean matched = false; boolean removed = false; boolean refreshed = false; for (java.util.Iterator<ToolUser.LoginToken> i = user.getLoginTokens().iterator(); i.hasNext();) { ToolUser.LoginToken loginToken = i.next(); if (loginToken.getToken().equals(token)) { if (loginToken.refreshTokenIfNecessary()) { refreshed = true; } matched = true; } else if (!loginToken.isValid()) { i.remove(); removed = true; } } if (!matched) { token = user.generateLoginToken(); } if (refreshed || removed) { user.save(); } } setSignedCookie(request, response, getToolUserCookieName(), token, -1, true); setSignedCookie(request, response, getInsecureToolUserCookieName(), token, -1, false); request.setAttribute(USER_ATTRIBUTE, user); request.setAttribute(USER_TOKEN, token); request.setAttribute(USER_CHECKED_ATTRIBUTE, Boolean.TRUE); } /** * Logs out the current tool user. * * @param request Can't be {@code null}. * @param response Can't be {@code null}. */ public static void logOut(HttpServletRequest request, HttpServletResponse response) { if (request != null) { ToolUser user = getUser(request); String token = (String) request.getAttribute(USER_TOKEN); if (user != null) { user.removeLoginToken(token); } } setSignedCookie(request, response, getToolUserCookieName(), "", 0, true); setSignedCookie(request, response, getInsecureToolUserCookieName(), "", 0, false); } /** * Logs out the current tool user. * * @param request Can't be {@code null}. * @param response Can't be {@code null}. * @deprecated Use {@link #logOut(HttpServletRequest, HttpServletResponse)} instead. */ @Deprecated public static void logOut(HttpServletResponse response) { logOut(PageContextFilter.Static.getRequest(), response); } /** * Returns {@code true} if a tool user is authenticated in the given * {@code request}. * * @param request Can't be {@code null}. */ public static boolean isAuthenticated(HttpServletRequest request) { return Boolean.TRUE.equals(request.getAttribute(AUTHENTICATED_ATTRIBUTE)); } public static boolean requireUser(ServletContext context, HttpServletRequest request, HttpServletResponse response) throws IOException { if (requireToolUrlPrefix(context, request, response)) { return true; } ToolUser user = getUser(request); if (user != null) { request.setAttribute(AUTHENTICATED_ATTRIBUTE, Boolean.TRUE); logIn(request, response, user); ForwardingDatabase db = new ForwardingDatabase() { @Override protected <T> Query<T> filterQuery(Query<T> query) { return query.clone().master().resolveInvisible().option(Database.DISABLE_FUNNEL_CACHE_QUERY_OPTION, true); } }; db.setDelegate(Database.Static.getDefault()); Database.Static.setIgnoreReadConnection(true); Database.Static.overrideDefault(db); request.setAttribute(DATABASE_OVERRIDDEN_ATTRIBUTE, Boolean.TRUE); ToolPageContext page = new ToolPageContext(context, request, response); String tfaUrl = page.cmsUrl("toolUserTfa"); String qrUrl = page.cmsUrl("qrCode"); if (user.isTfaRequired() && !user.isTfaEnabled() && !request.getRequestURI().startsWith(tfaUrl) && !request.getRequestURI().startsWith(qrUrl)) { user = Query.from(ToolUser.class).where("_id = ?", user.getId()).noCache().master().first(); if (user != null && user.isTfaRequired() && !user.isTfaEnabled()) { tfaUrl = page.cmsUrl("toolUserTfa", RETURN_PATH_PARAMETER, JspUtils.getAbsolutePath(request, "")); response.sendRedirect(tfaUrl); return true; } } } else if (!JspUtils.getEmbeddedServletPath(context, request.getServletPath()).equals(LOG_IN_PATH)) { @SuppressWarnings("resource") ToolPageContext page = new ToolPageContext(context, request, response); String loginUrl = page.cmsUrl(LOG_IN_PATH, RETURN_PATH_PARAMETER, JspUtils.getAbsolutePath(request, "")); response.sendRedirect(loginUrl); return true; } Cookie csrfCookie = JspUtils.getCookie(request, "bsp.csrf"); if (csrfCookie == null) { csrfCookie = new Cookie("bsp.csrf", UUID.randomUUID().toString()); } csrfCookie.setMaxAge(-1); csrfCookie.setPath("/"); csrfCookie.setSecure(JspUtils.isSecure(request)); response.addCookie(csrfCookie); if (JspUtils.isFormPost(request) && !csrfCookie.getValue().equals(ObjectUtils.firstNonNull( request.getHeader("Brightspot-CSRF"), request.getParameter("_csrf")))) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return true; } return false; } public static boolean requireToolUrlPrefix(ServletContext context, HttpServletRequest request, HttpServletResponse response) throws IOException { String toolUrlPrefix = Settings.get(String.class, ToolPageContext.TOOL_URL_PREFIX_SETTING); if (!ObjectUtils.isBlank(toolUrlPrefix) && !new UrlBuilder(request) .currentScheme() .currentHost() .currentPath() .toString() .startsWith(toolUrlPrefix)) { response.sendRedirect( StringUtils.removeEnd(toolUrlPrefix, "/") + new UrlBuilder(request) .currentPath() .currentParameters() .toString()); return true; } return false; } private static ToolUser getToolUserByCookieName( HttpServletRequest request, String cookieName, String toolUserAttribute, String toolUserCheckedAttribute) { ToolUser toolUser; if (Boolean.TRUE.equals(request.getAttribute(toolUserCheckedAttribute))) { toolUser = (ToolUser) request.getAttribute(toolUserAttribute); } else { long sessionTimeout = ObjectUtils.firstNonNull( Settings.get(Long.class, TOOL_USER_SESSION_TIMEOUT_SETTING), Settings.getOrDefault(long.class, "cms/tool/sessionTimeout", 0L)); String cookieValue = JspUtils.getSignedCookieWithExpiry(request, cookieName, sessionTimeout); if (cookieValue == null || cookieValue.length() < cookieName.length()) { toolUser = null; } else { String token = cookieValue.substring(cookieName.length()); toolUser = ToolUser.Static.getByToken(token); request.setAttribute(USER_TOKEN, token); request.setAttribute(toolUserAttribute, toolUser); } request.setAttribute(toolUserCheckedAttribute, Boolean.TRUE); } return toolUser; } /** * Returns the tool user associated with the given {@code request}. * * @param request Can't be {@code null}. */ public static ToolUser getUser(HttpServletRequest request) { return getToolUserByCookieName( request, getToolUserCookieName(), TOOL_USER_ATTRIBUTE, TOOL_USER_CHECKED_ATTRIBUTE); } /** * Returns the possible and probably insecure tool user associated * with the given {@code request}. * * @param request Can't be {@code null}. */ public static ToolUser getInsecureToolUser(HttpServletRequest request) { return getToolUserByCookieName( request, getInsecureToolUserCookieName(), INSECURE_TOOL_USER_ATTRIBUTE, INSECURE_TOOL_USER_CHECKED_ATTRIBUTE); } public static Preview getCurrentPreview(HttpServletRequest request) { if (getUser(request) == null) { return null; } Preview preview; if (Boolean.TRUE.equals(request.getAttribute(PREVIEW_CHECKED_ATTRIBUTE))) { preview = (Preview) request.getAttribute(PREVIEW_ATTRIBUTE); } else { String previewCookie = getPreviewCookieName(); String cookieValue = JspUtils.getSignedCookie(request, previewCookie); if (cookieValue == null || cookieValue.length() < previewCookie.length()) { preview = null; } else { preview = Query .from(Preview.class) .where("_id = ?", ObjectUtils.to(UUID.class, cookieValue.substring(previewCookie.length()))) .first(); request.setAttribute(PREVIEW_ATTRIBUTE, preview); } request.setAttribute(PREVIEW_CHECKED_ATTRIBUTE, Boolean.TRUE); } return preview; } public static void setCurrentPreview(HttpServletRequest request, HttpServletResponse response, Preview preview) { setSignedCookie(request, response, getPreviewCookieName(), preview.getId().toString(), -1, true); request.setAttribute(PREVIEW_ATTRIBUTE, preview); request.setAttribute(PREVIEW_CHECKED_ATTRIBUTE, Boolean.TRUE); } public static void removeCurrentPreview(HttpServletRequest request, HttpServletResponse response) { setSignedCookie(request, response, getPreviewCookieName(), "", 0, true); request.removeAttribute(PREVIEW_ATTRIBUTE); request.removeAttribute(PREVIEW_CHECKED_ATTRIBUTE); } /** * Returns the user setting value associated with the given * {@code key}. */ public static Object getUserSetting(HttpServletRequest request, String key) { ToolUser user = getUser(request); return user != null ? user.getSettings().get(key) : null; } /** * Puts the given user setting {@code value} at the given {@code key}. * The user, along with the setting values, are saved once at the end * of the given {@code request}. */ public static void putUserSetting(HttpServletRequest request, String key, Object value) { ToolUser user = getUser(request); if (user != null) { user.getSettings().put(key, value); request.setAttribute(USER_SETTINGS_CHANGED_ATTRIBUTE, Boolean.TRUE); } } // Returns the page setting key for use with the given {@code request} // and {@code key}. private static String getPageSettingKey(HttpServletRequest request, String key) { return "page" + request.getServletPath() + "/" + key; } /** * Returns the page setting value associated with the given * {@code request} and {@code key}. */ public static Object getPageSetting(HttpServletRequest request, String key) { return getUserSetting(request, getPageSettingKey(request, key)); } /** * Puts the given page setting {@code value} at the given * {@code request} and {@code key}. The user, along with the setting * values, are saved once at the end of the given {@code request}. */ public static void putPageSetting(HttpServletRequest request, String key, Object value) { putUserSetting(request, getPageSettingKey(request, key), value); } } }