All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.webswing.server.services.security.modules.AbstractSecurityModule Maven / Gradle / Ivy

The newest version!
package org.webswing.server.services.security.modules;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webswing.server.common.service.security.AbstractWebswingUser;
import org.webswing.server.common.service.security.AuthenticatedWebswingUser;
import org.webswing.server.common.service.security.impl.WebswingSecuritySubject;
import org.webswing.server.common.util.ServerUtil;
import org.webswing.server.common.util.WebswingObjectMapper;
import org.webswing.server.services.security.api.LoginResponseClosedException;
import org.webswing.server.services.security.api.WebswingAuthenticationException;
import org.webswing.server.services.security.api.WebswingSecurityModule;
import org.webswing.server.services.security.api.WebswingSecurityModuleConfig;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheResolver;

/**
 * 

* Abstract implementation of {@link WebswingSecurityModule} that offers convenience methods for * proper handling of Ajax and non-Ajax requests and implementing the default login flow for {@link WebswingSecurityModule#doLogin(HttpServletRequest, HttpServletResponse) doLogin}. *

*

* By default, Webswing's javascript client use Ajax call for authentication. This implicates that the login page * served by the security module should be a partial HTML containing only the login form. On the other hand if user is redirected * to the /login url, module should respond with full HTML login page. Two abstract methods: {@link #serveLoginPartial(HttpServletRequest, HttpServletResponse, WebswingAuthenticationException) serveLoginPartial} * and {@link #serveLoginPage(HttpServletRequest, HttpServletResponse, WebswingAuthenticationException) serveLoginPage} are called as appropriate for this purpose. *

*

* For implementing mentioned abstract methods use the {@link AbstractSecurityModule#sendHtml(HttpServletRequest, HttpServletResponse, String, Object) sendHtml} method, which * uses Mustache templating syntax to help you respond with dynamic pages. *

*

* In case the login workflow needs to perform a full redirect (see SAML2 implementation), it has to send a response recognized by the ajax caller. * For that purpose use {@link AbstractSecurityModule#sendRedirect(HttpServletRequest, HttpServletResponse, String)} method. *

* * @param Interface for reading security module's JSON configuration. */ /** * @author vikto * * @param */ public abstract class AbstractSecurityModule implements WebswingSecurityModule { private static final Logger auditLog = LoggerFactory.getLogger(WebswingSecurityModule.class); /** * Login response message parameter to indicate url redirect.(for Ajax calls) */ public static final String REDIRECT_URL = "redirectUrl"; /** * Login request message parameter to indicate redirection upon successful login. */ public static final String SUCCESS_URL = "successUrl"; private static final Logger log = LoggerFactory.getLogger(AbstractSecurityModule.class); private static final String LOGIN_REQUEST_MSG = "LoginRequestMsg"; private final T config; private final DefaultMustacheFactory mf; private final Map compiledTemplates = new HashMap<>(); public AbstractSecurityModule(T config) { this.config = config; mf = new DefaultMustacheFactory(new MustacheResolver() { @Override public Reader getReader(String resourceName) { URL url = findTemplate(resourceName); if (url != null) { try { InputStream is = url.openStream(); return new InputStreamReader(is); } catch (IOException e) { log.error("Failed to open Template from url:" + url); } } return null; } }); } public T getConfig() { return config; } @Override public void init() { } @Override public void destroy() { compiledTemplates.clear(); } @Override public AuthenticatedWebswingUser doLogin(HttpServletRequest request, HttpServletResponse response, String securedPath) throws IOException { try { Map msg = getLoginRequest(request); if (msg != null) { config.getContext().setToSecuritySession(LOGIN_REQUEST_MSG, msg); } preVerify(request, response); AuthenticatedWebswingUser user = authenticate(request); if (user != null) { postVerify(user, request, response); user= decorateUser(user, request, response); onAuthenticationSuccess(user, request, response, securedPath); return user; } onAuthenticationFailed(request, response, null); } catch (WebswingAuthenticationException e) { onAuthenticationFailed(request, response, e); } catch (LoginResponseClosedException e) { return null; } return null; } @Override public void doLogout(HttpServletRequest req, HttpServletResponse res, AbstractWebswingUser user) throws ServletException, IOException { doLogout(req, res); } public void doLogout(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String fullPath = getConfig().getContext().getSecuredPath(); sendRedirect(req, res, fullPath); } public void logoutRedirect(HttpServletRequest request, HttpServletResponse response, String logoutUrl) throws IOException { if (logoutUrl != null) { sendRedirect(request, response, logoutUrl); } else { sendPartialHtml(request, response, "logoutPartial.html", null); } } @Override public void doServeAuthenticated(AbstractWebswingUser user, String path, HttpServletRequest req, HttpServletResponse res) throws IOException { res.setStatus(HttpServletResponse.SC_OK); res.setHeader("webswingUsername", user.getUserId()); serveAuthenticated(user, path, req, res); } protected void serveAuthenticated(AbstractWebswingUser user, String path, HttpServletRequest req, HttpServletResponse res) { } /** * See {@link AbstractExtendableSecurityModule} */ protected AuthenticatedWebswingUser decorateUser(AuthenticatedWebswingUser user, HttpServletRequest request, HttpServletResponse response) { return user; } /** * See {@link AbstractExtendableSecurityModule} */ protected void postVerify(AuthenticatedWebswingUser user, HttpServletRequest request, HttpServletResponse response) throws LoginResponseClosedException, WebswingAuthenticationException { } /** * See {@link AbstractExtendableSecurityModule} */ protected void preVerify(HttpServletRequest request, HttpServletResponse response) throws LoginResponseClosedException, WebswingAuthenticationException { } /** * Check if request has any login credentials. If it does and they are valid return an instance of {@link AuthenticatedWebswingUser} * otherwise throw {@link WebswingAuthenticationException}. * If no credentials are present return null. * * @param request Login request * @return authenticated user or null * @throws WebswingAuthenticationException if authentication failed. */ protected abstract AuthenticatedWebswingUser authenticate(HttpServletRequest request) throws WebswingAuthenticationException; /** * If the login request is not Ajax call and a {@link #SUCCESS_URL} was sent with first request, * send redirect to this url. * @param user authenticated user * @param request login request * @param response login response * @throws IOException if fails to respond */ protected void onAuthenticationSuccess(AuthenticatedWebswingUser user, HttpServletRequest request, HttpServletResponse response, String securedPath) throws IOException { response.setStatus(HttpServletResponse.SC_OK); response.setHeader("webswingUsername", user.getUserId()); WebswingSecuritySubject subject = WebswingSecuritySubject.get(); subject.login(response, securedPath, user); if (!isAjax(request)) { Map msg = getLoginRequest(request); if (msg != null && msg.containsKey(SUCCESS_URL)) { sendRedirect(request, response, (String) msg.get(SUCCESS_URL)); } else { String defaultPath = config.getContext().getSecuredPath(); sendRedirect(request, response, defaultPath); } } } /** * Send {@link HttpServletResponse#SC_UNAUTHORIZED} response status and serve full login page or partial login page if * request was initiated by Ajax call. * @param request * @param response * @param exception * @throws IOException */ protected void onAuthenticationFailed(HttpServletRequest request, HttpServletResponse response, WebswingAuthenticationException exception) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); WebswingSecuritySubject.get().saveLoginSession(response); if (isAjax(request)) { serveLoginPartial(request, response, exception); } else { serveLoginPage(request, response, exception); } } /** * Respond with full login HTML page. If exception is not null, page should indicate the error message. * Use {@link #sendHtml(HttpServletRequest, HttpServletResponse, String, Object) sendHtml} helper method. * @param request login request * @param response login response * @param exception null or exception thrown by previous login attempt. * @throws IOException if fails to send response */ protected void serveLoginPage(HttpServletRequest request, HttpServletResponse response, WebswingAuthenticationException exception) throws IOException { serveLoginPartial(request, response, exception); } /** * Respond with partial login HTML page. If exception is not null, page should indicate the error message. * Use {@link #sendPartialHtml(HttpServletRequest, HttpServletResponse, String, Object) sendHtml} helper method. * @param request login request * @param response login response * @param exception null or exception thrown by previous login attempt. * @throws IOException if fails to send response */ protected abstract void serveLoginPartial(HttpServletRequest request, HttpServletResponse response, WebswingAuthenticationException exception) throws IOException; /** * Detect if request have been sent using Ajax call or regular browser request * @param request login request * @return true if ajax header is found */ protected boolean isAjax(HttpServletRequest request) { String requestedWithHeader = request.getHeader("X-Requested-With"); if (requestedWithHeader != null && requestedWithHeader.equals("XMLHttpRequest")) { return true; } return false; } /** * Sends 302 redirect response or redirect JSON message if request is Ajax call. * @param request login request * @param response login response * @param url Redirect URL * @throws IOException if fails to send response */ protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException { if (isAjax(request)) { Map message = new HashMap<>(); message.put(REDIRECT_URL, url); try { WebswingObjectMapper.get().writeValue(response.getOutputStream(), message); } catch (Exception e) { throw new IOException("Failed to send login redirect message", e); } } else { ServerUtil.sendHttpRedirect(request, response, url); } } /** * Processes the template ad sends the result HTML or if request is Ajax call, sends the processed * template HTML as JSON response. * @param request login request * @param response login response * @param template path to template file.(See {@link #findTemplate(String)}) * @param variables java POJO object or instance of Map, that will be used to replace variables in template files. * @throws IOException if fails to send response */ protected void sendHtml(HttpServletRequest request, HttpServletResponse response, String template, Object variables) throws IOException { Object[] extendedVars = new Object[] { variables, getDefaultVariables(request) }; if (isAjax(request)) { Map message = new HashMap<>(); try { Writer w = new StringWriter(); processTemplate(w, template, extendedVars); message.put("partialHtml", w.toString()); WebswingObjectMapper.get().writeValue(response.getOutputStream(), message); } catch (Exception e) { throw new IOException("Failed to send login template message", e); } } else { response.setContentType("text/html"); Writer w = new OutputStreamWriter(response.getOutputStream()); processTemplate(w, template, extendedVars); } } /** * If request is not Ajax call, template is wrapped into default HTML page. * If request is Ajax call, sends the template in JSON response. * @param request login request * @param response login response * @param template path to partial template file.(See {@link #findTemplate(String)}) * @param variables java POJO object or instance of Map, that will be used to replace variables in template files. * @throws IOException if fails to send response */ protected void sendPartialHtml(HttpServletRequest request, HttpServletResponse response, String template, Object variables) throws IOException { Map defaultVars = getDefaultVariables(request); Object[] extendedVars = new Object[] { variables, defaultVars }; if (isAjax(request)) { sendHtml(request, response, template, variables); } else { response.setContentType("text/html;charset=UTF-8"); Writer w = new OutputStreamWriter(response.getOutputStream()); Writer tempw = new StringWriter(); processTemplate(tempw, template, extendedVars); defaultVars.put("partialHtml", new String(Base64.getEncoder().encode(tempw.toString().getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)); processTemplate(w, "default.html", extendedVars); } } private Map getDefaultVariables(HttpServletRequest request) { Map result = new HashMap<>(); result.put("requestBaseUrl", getBaseUrl(request)); return result; } private String getBaseUrl(HttpServletRequest req) { String proto = req.getHeader("X-Forwarded-Proto"); if (proto == null) { proto = req.getRequestURL().toString().startsWith("https") ? "https" : "http"; } String host = req.getHeader("X-Forwarded-Host"); if (host == null) { host = req.getServerName(); int port = req.getServerPort(); if (port != 80 && port != 443) { host += ":" + port; } } String result = proto + "://" + host + config.getContext().getSecuredPath(); return result; } /** * Default implementation uses Mustache style templates. Override this method if other template * framework is needed. * @param w processed output will be written here * @param template path to template file.(See {@link #findTemplate(String)}) * @param variables java POJO object or instance of Map, that will be used to replace variables in template files. * @throws IOException */ protected void processTemplate(Writer w, String template, Object[] variables) throws IOException { try { if (template != null && w != null) { Mustache mustache = compileTemplate(template); mustache.execute(w, variables); w.flush(); } } finally { if (w != null) { w.close(); } } } private Mustache compileTemplate(String name) throws IOException { Mustache mustache = compiledTemplates.get(name); if (mustache == null) { mustache = mf.compile(name); compiledTemplates.put(name, mustache); } return mustache; } /** * Defines where to look for template files. * Default implementation looks for template in web resource first, and if not found * it tries to load it from classpath of Security Module. * @param name path to template * @return template URL */ public URL findTemplate(String name) { URL url = getConfig().getContext().getWebResource(name); if (url == null) { url = getClass().getClassLoader().getResource(name); } return url; } /** * Reads the initial Ajax login request message from request or from session. * @param request login request * @return login request parameter map */ @SuppressWarnings("unchecked") public Map getLoginRequest(HttpServletRequest request) { if (isAjax(request) && "application/json".equals(request.getContentType())) { try { if (request.getAttribute(LOGIN_REQUEST_MSG) == null) { Map loginRequest = WebswingObjectMapper.get().readValue(request.getReader(), Map.class); request.setAttribute(LOGIN_REQUEST_MSG, loginRequest); } return (Map) request.getAttribute(LOGIN_REQUEST_MSG); } catch (Exception e) { log.debug("Failed to read login request data.", e); } } return (Map) getConfig().getContext().getFromSecuritySession(LOGIN_REQUEST_MSG); } /** * @return JSON serializer. */ public static WebswingObjectMapper getMapper() { return WebswingObjectMapper.get(); } public void logSuccess(HttpServletRequest r, String user) { String path = getConfig().getContext().getSecuredPath(); path = StringUtils.isEmpty(path) ? "/" : path; String module = this.getClass().getName(); auditLog("SUCCESS", r, path, module, user, ""); } public void logFailure(HttpServletRequest r, String user, String reason) { String path = getConfig().getContext().getSecuredPath(); path = StringUtils.isEmpty(path) ? "/" : path; String module = this.getClass().getName(); auditLog("FAILED", r, path, module, user, reason); } public String replaceVar(String s) { return getConfig().getContext().replaceVariables(s); } public static void auditLog(String status, HttpServletRequest r, String path, String module, String username, String reason) { String protocol = r.getScheme(); String ipAddress = r.getHeader("X-FORWARDED-FOR"); if (ipAddress == null) { ipAddress = r.getRemoteAddr(); } auditLog.info("{} | {} | {} | {} | {} | {} | {}", new Object[] { status, username, reason, path, protocol, ipAddress, module }); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy