Commit 08fe2fd8 by Tuomas Riihimäki

Refactor rest auth to bean level, so it can be used in multiple places

1 parent 4d7f6b40
package fi.codecrew.moya.beans.auth;
import javax.ejb.Local;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@Local
public interface AuthHelperBeanLocal {
String parseHostname(HttpServletRequest httpRequest);
RestAuthResponse parseRestAuthCredentials(HttpServletRequest httpRequest) throws ServletException;
class RestAuthResponse {
public final String username;
public final String password;
public RestAuthResponse(String username, String password) {
this.username = username;
this.password = password;
}
}
}
package fi.codecrew.moya.beans.auth;
import fi.codecrew.moya.beans.ApiApplicationBeanLocal;
import fi.codecrew.moya.beans.JaasBeanLocal;
import fi.codecrew.moya.beans.LoggingBeanLocal;
import fi.codecrew.moya.utilities.UserLoginUtils;
import fi.codecrew.moya.utilities.moyamessage.MoyaEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ejb.EJB;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Base64;
@LocalBean
@Stateless
public class AuthHelperBean implements AuthHelperBeanLocal {
private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
private static final Logger logger = LoggerFactory.getLogger(AuthHelperBean.class);
@EJB
private LoggingBeanLocal logbean;
@EJB
private ApiApplicationBeanLocal apibean;
@Override
public String parseHostname(HttpServletRequest httpRequest) {
StringBuffer url = httpRequest.getRequestURL();
String requestHostHeader = httpRequest.getHeader("host");
String hostname = "localhost";
if (requestHostHeader != null) {
hostname = requestHostHeader.split(":")[0];
}
// if proxy provides scheme in header, use it..
String scheme = httpRequest.getHeader(X_FORWARDED_PROTO);
if (scheme == null || scheme.isEmpty()) {
scheme = url.substring(0, 5).toLowerCase();
}
Principal principal = httpRequest.getUserPrincipal();
if (principal != null) {
String userDomain = UserLoginUtils.getDomainFromJaas(principal);
// If there is no logged-in user, we can and should not check userDomain against hostname
if (principal != null && !hostname.equals(userDomain)) {
logbean.sendMessage(MoyaEventType.USER_PERMISSION_VIOLATION,
"Hostname mismatch privilege escalation! User '", httpRequest.getUserPrincipal(), "' tried to change hostname from '",
userDomain, "' to '", hostname, ",");
throw new RuntimeException("Hostname mismatch! Expected: " + hostname + " but logged in as " + userDomain);
}
}
return hostname;
}
/**
* Check if we should
*
* @param httpRequest
* @return Return wether rest failed. If it was not tried, function will return true, which
* will allow other authentication methods to be tried.
*/
public AuthHelperBeanLocal.RestAuthResponse parseRestAuthCredentials(HttpServletRequest httpRequest) throws ServletException {
// Allow api auth with password only if ssl is enabled ( or in
// development mode... )
// if (BortalLocalContextHolder.isSsl() ||
// BortalLocalContextHolder.isInDevelopmentMode()) {
String restAuthStr = httpRequest.getHeader("Authorization");
// }
String userkey = null;
String appkey = null;
// Payara got updated, and does not allow changing of username anymore, so we must send
// username and domain to initial jaas-login query...
if (restAuthStr != null) {
String decodedStr = new String(Base64.getDecoder().decode(restAuthStr.split(" ")[1]), StandardCharsets.UTF_8);
String[] splitStr = decodedStr.split(":");
appkey = splitStr[1];
userkey = splitStr[2];
// final String appKey = splitStr[3];
} else if (httpRequest.getParameter("appkey") != null) {
appkey = httpRequest.getParameter("appkey");
userkey = httpRequest.getParameter("appuser");
StringBuilder hashBuilder = new StringBuilder();
hashBuilder.append(JaasBeanLocal.REST_PREFIX);
hashBuilder.append(appkey).append(":");
hashBuilder.append(userkey).append(":");
hashBuilder.append(httpRequest.getParameter("appstamp")).append(":");
hashBuilder.append(httpRequest.getParameter("appmac")).append(":");
hashBuilder.append(httpRequest.getPathInfo());
restAuthStr = hashBuilder.toString();
} else {
// If no rest authentication is even tried, allow, anon login
return null;
}
String domain = parseHostname(httpRequest);
if (restAuthStr == null) {
throw new ServletException("No auth data");
}
//final String username = "@" + parseHostname(httpRequest);
String userLogin = apibean.findUsernameForApikey(appkey, userkey, domain);
if (userLogin == null) {
throw new ServletException("Unable to find username for credentials");
}
return new RestAuthResponse(userLogin + '@' + domain, restAuthStr);
}
}
...@@ -18,37 +18,28 @@ ...@@ -18,37 +18,28 @@
*/ */
package fi.codecrew.moya; package fi.codecrew.moya;
import java.io.IOException; import fi.codecrew.moya.beans.*;
import java.nio.charset.Charset; import fi.codecrew.moya.beans.auth.AuthHelperBeanLocal;
import java.nio.charset.StandardCharsets; import fi.codecrew.moya.clientutils.BortalLocalContextHolder;
import java.security.Principal; import fi.codecrew.moya.model.User;
import java.util.Base64; import fi.codecrew.moya.rest.RestApplicationEntrypoint;
import fi.codecrew.moya.utilities.RestAuthHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import javax.ejb.EJB; import javax.ejb.EJB;
import javax.faces.application.ProjectStage; import javax.faces.application.ProjectStage;
import javax.faces.context.FacesContext; import javax.faces.context.FacesContext;
import javax.servlet.Filter; import javax.servlet.*;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import java.io.IOException;
import java.nio.charset.Charset;
import fi.codecrew.moya.beans.*; import java.nio.charset.StandardCharsets;
import fi.codecrew.moya.utilities.UserLoginUtils; import java.security.Principal;
import org.apache.commons.codec.Charsets; import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import fi.codecrew.moya.clientutils.BortalLocalContextHolder;
import fi.codecrew.moya.model.User;
import fi.codecrew.moya.rest.RestApplicationEntrypoint;
import fi.codecrew.moya.utilities.moyamessage.MoyaEventType;
/** /**
* Servlet Filter implementation class HostnameFilter * Servlet Filter implementation class HostnameFilter
...@@ -56,13 +47,15 @@ import fi.codecrew.moya.utilities.moyamessage.MoyaEventType; ...@@ -56,13 +47,15 @@ import fi.codecrew.moya.utilities.moyamessage.MoyaEventType;
@WebFilter(filterName = "hostnameFilter", displayName = "hostname and authentication filter", urlPatterns = {"/*"}) @WebFilter(filterName = "hostnameFilter", displayName = "hostname and authentication filter", urlPatterns = {"/*"})
public class HostnameFilter implements Filter { public class HostnameFilter implements Filter {
private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
private static final String SESSION_HOSTNAMESTORE = "moya-session-hostname"; private static final String SESSION_HOSTNAMESTORE = "moya-session-hostname";
private static final Logger logger = LoggerFactory.getLogger(HostnameFilter.class); private static final Logger logger = LoggerFactory.getLogger(HostnameFilter.class);
private static final String HTTP_HOSTNAME_ID = "moya_hostname_session_id"; private static final String HTTP_HOSTNAME_ID = "moya_hostname_session_id";
private boolean developmentMode = false; private boolean developmentMode = false;
@EJB @EJB
private AuthHelperBeanLocal authHelper;
@EJB
private RestBeanLocal restauth; private RestBeanLocal restauth;
@EJB @EJB
private LoggingBeanLocal logbean; private LoggingBeanLocal logbean;
...@@ -169,13 +162,14 @@ public class HostnameFilter implements Filter { ...@@ -169,13 +162,14 @@ public class HostnameFilter implements Filter {
chain.doFilter(request, response); chain.doFilter(request, response);
return; return;
} }
BortalLocalContextHolder.setInDevelopmentMode(developmentMode);
try { try {
httpRequest = (HttpServletRequest) request; httpRequest = (HttpServletRequest) request;
insertServerLoggingContext(httpRequest, authtype); insertServerLoggingContext(httpRequest, authtype);
logger.info("Logging in with username {} and password {}, remote {}, authtype: {}", httpRequest.getUserPrincipal(), httpRequest.getRemoteUser(), httpRequest.getAuthType()); logger.info("Logging in with username {} and password {}, remote {}, authtype: {}", httpRequest.getUserPrincipal(), httpRequest.getRemoteUser(), httpRequest.getAuthType());
String hostname = parseHostname(httpRequest); String hostname = authHelper.parseHostname(httpRequest);
if (httpRequest.getUserPrincipal() == null) { if (httpRequest.getUserPrincipal() == null) {
// Check if we are can login in with rest alternative methods ( appkey, basic auth, etc.. ) // Check if we are can login in with rest alternative methods ( appkey, basic auth, etc.. )
...@@ -184,9 +178,9 @@ public class HostnameFilter implements Filter { ...@@ -184,9 +178,9 @@ public class HostnameFilter implements Filter {
authtype = AuthType.REST; authtype = AuthType.REST;
if (!restAuth(httpRequest, response) && RestApplicationEntrypoint.REST_PATH.equals(httpRequest.getServletPath())) { if (!tryRestAuth(httpRequest, response) && RestApplicationEntrypoint.REST_PATH.equals(httpRequest.getServletPath())) {
response.reset(); response.reset();
response.getOutputStream().write("Rest auth failed! ".getBytes( StandardCharsets.UTF_8)); response.getOutputStream().write("Rest auth failed! ".getBytes(StandardCharsets.UTF_8));
if (response instanceof HttpServletResponse) { if (response instanceof HttpServletResponse) {
HttpServletResponse httpResp = (HttpServletResponse) response; HttpServletResponse httpResp = (HttpServletResponse) response;
...@@ -231,19 +225,12 @@ public class HostnameFilter implements Filter { ...@@ -231,19 +225,12 @@ public class HostnameFilter implements Filter {
} }
} }
// public static String getCurrentHostname(HttpSession sess) { /**
// String ret = null; * @param httpRequest
// if (sess != null) { * @param response
// Object retObj = sess.getAttribute(EventBeanLocal.HTTP_URL_HOSTNAME); * @return Return false, if rest auth failed, and no other authenthication methods should be tried
// if (retObj != null) { */
// ret = retObj.toString(); private boolean tryRestAuth(HttpServletRequest httpRequest, ServletResponse response) {
// }
// }
// return ret;
// }
private boolean restAuth(HttpServletRequest httpRequest, ServletResponse response) {
String sp = httpRequest.getPathInfo(); String sp = httpRequest.getPathInfo();
for (String s : NOAUTH_RESTPATHS) { for (String s : NOAUTH_RESTPATHS) {
if (sp.startsWith(s)) { if (sp.startsWith(s)) {
...@@ -251,99 +238,29 @@ public class HostnameFilter implements Filter { ...@@ -251,99 +238,29 @@ public class HostnameFilter implements Filter {
} }
} }
String restAuthStr = null;
// Allow api auth with password only if ssl is enabled ( or in
// development mode... )
// if (BortalLocalContextHolder.isSsl() ||
// BortalLocalContextHolder.isInDevelopmentMode()) {
restAuthStr = httpRequest.getHeader("Authorization");
// }
String userkey = null;
String appkey = null;
// Payara got updated, and does not allow changing of username anymore, so we must send
// username and domain to initial jaas-login query...
if (restAuthStr != null) {
String decodedStr = new String(Base64.getDecoder().decode(restAuthStr.split(" ")[1]), UTF8);
String[] splitStr = decodedStr.split(":");
appkey = splitStr[1];
userkey = splitStr[2];
// final String appKey = splitStr[3];
} else if (httpRequest.getParameter("appkey") != null) {
appkey = httpRequest.getParameter("appkey");
userkey = httpRequest.getParameter("appuser");
StringBuilder hashBuilder = new StringBuilder();
hashBuilder.append(JaasBeanLocal.REST_PREFIX);
hashBuilder.append(appkey).append(":");
hashBuilder.append(userkey).append(":");
hashBuilder.append(httpRequest.getParameter("appstamp")).append(":");
hashBuilder.append(httpRequest.getParameter("appmac")).append(":");
hashBuilder.append(httpRequest.getPathInfo());
restAuthStr = hashBuilder.toString();
} else {
// If no rest authentication is even tried, allow, anon login
return true;
}
boolean ret = true;
String domain = parseHostname(httpRequest);
try { try {
if (restAuthStr == null) {
throw new ServletException("No auth data"); AuthHelperBeanLocal.RestAuthResponse creds = authHelper.parseRestAuthCredentials(httpRequest);
} if (creds == null) {
//final String username = "@" + parseHostname(httpRequest); return true;
String userLogin = apibean.findUsernameForApikey(appkey, userkey, domain);
if (userLogin != null) {
httpRequest.login(userLogin + '@' + domain, restAuthStr);
} }
httpRequest.login(creds.username, creds.password);
Principal p = httpRequest.getUserPrincipal(); Principal p = httpRequest.getUserPrincipal();
logger.warn("Logged in with rest:{}, ", (p == null) ? null : p.getName()); logger.warn("Logged in with rest: {}, ", (p == null) ? null : p.getName());
} catch (ServletException loginEx) { } catch (ServletException loginEx) {
ret = false;
logger.info("Rest api authentication failed for path " + httpRequest.getPathInfo() + " " logger.info("Rest api authentication failed for path " + httpRequest.getPathInfo() + " "
+ httpRequest.getParameterMap().toString(), loginEx); + httpRequest.getParameterMap().toString(), loginEx);
if (response instanceof HttpServletResponse) { if (response instanceof HttpServletResponse) {
HttpServletResponse httpResp = ((HttpServletResponse) response); HttpServletResponse httpResp = ((HttpServletResponse) response);
httpResp.setStatus(HttpServletResponse.SC_FORBIDDEN); httpResp.setStatus(HttpServletResponse.SC_FORBIDDEN);
} }
return false;
} }
return ret; return true;
} }
protected String parseHostname(HttpServletRequest httpRequest) {
StringBuffer url = httpRequest.getRequestURL();
String requestHostHeader = httpRequest.getHeader("host");
String hostname = "localhost";
if (requestHostHeader != null) {
hostname = requestHostHeader.split(":")[0];
}
// if proxy provides scheme in header, use it..
String scheme = httpRequest.getHeader(X_FORWARDED_PROTO);
if (scheme == null || scheme.isEmpty()) {
scheme = url.substring(0, 5).toLowerCase();
}
Principal principal = httpRequest.getUserPrincipal();
if (principal != null) {
String userDomain = UserLoginUtils.getDomainFromJaas(principal);
// If there is no logged-in user, we can and should not check userDomain against hostname
if (principal != null && !hostname.equals(userDomain)) {
logbean.sendMessage(MoyaEventType.USER_PERMISSION_VIOLATION,
"Hostname mismatch privilege escalation! User '", httpRequest.getUserPrincipal(), "' tried to change hostname from '",
userDomain, "' to '", hostname, ",");
throw new RuntimeException("Hostname mismatch! Expected: " + hostname + " but logged in as " + userDomain);
}
}
BortalLocalContextHolder.setInDevelopmentMode(developmentMode);
return hostname;
}
} }
...@@ -26,6 +26,7 @@ import javax.ejb.EJB; ...@@ -26,6 +26,7 @@ import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped; import javax.enterprise.context.RequestScoped;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part; import javax.servlet.http.Part;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
...@@ -43,6 +44,7 @@ import javax.ws.rs.core.Response; ...@@ -43,6 +44,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import fi.codecrew.moya.beans.auth.AuthHelperBean;
import fi.codecrew.moya.model.*; import fi.codecrew.moya.model.*;
import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
...@@ -105,6 +107,8 @@ public class UserRestView { ...@@ -105,6 +107,8 @@ public class UserRestView {
@EJB @EJB
private PlaceBeanLocal placebean; private PlaceBeanLocal placebean;
@EJB
private AuthHelperBean authHelperBean;
@POST @POST
@Path("/giveplace/{placeId}") @Path("/giveplace/{placeId}")
...@@ -222,12 +226,11 @@ public class UserRestView { ...@@ -222,12 +226,11 @@ public class UserRestView {
} }
} }
if (principal == null || principal.getName() == null || !principal.getName().equals(username)) { if (principal == null || principal.getName() == null || !principal.getName().equals(username)) {
String requestHostHeader = servletRequest.getHeader("host");
String hostname = null; String hostname = authHelperBean.parseHostname(servletRequest);
if (requestHostHeader != null) { HttpSession session = servletRequest.getSession(true);
hostname = requestHostHeader.split(":")[0]; logger.warn("Session Id {}", session.getId());
}
servletRequest.getSession(true);
servletRequest.login(username + '@' + hostname, password); servletRequest.login(username + '@' + hostname, password);
} }
} catch (ServletException e) { } catch (ServletException e) {
......
...@@ -73,7 +73,7 @@ public class ApiAppRestViewV1 { ...@@ -73,7 +73,7 @@ public class ApiAppRestViewV1 {
return Response.ok(ret).build(); return Response.ok(ret).build();
} catch (ServletException e) { } catch (ServletException e) {
logger.warn("Error logging in while creating ApiApplication instance"); logger.warn("Error logging in while creating ApiApplication instance",e);
return Response.serverError().entity(e.getCause()).build(); return Response.serverError().entity(e.getCause()).build();
} }
} }
......
...@@ -2,6 +2,7 @@ package fi.codecrew.moya.rest.appconfig.v1; ...@@ -2,6 +2,7 @@ package fi.codecrew.moya.rest.appconfig.v1;
import fi.codecrew.moya.beans.EventBeanLocal; import fi.codecrew.moya.beans.EventBeanLocal;
import fi.codecrew.moya.beans.PermissionBeanLocal; import fi.codecrew.moya.beans.PermissionBeanLocal;
import fi.codecrew.moya.beans.auth.AuthHelperBeanLocal;
import fi.codecrew.moya.rest.PojoUtils; import fi.codecrew.moya.rest.PojoUtils;
import fi.codecrew.moya.rest.pojo.appconfig.v1.EventRoot; import fi.codecrew.moya.rest.pojo.appconfig.v1.EventRoot;
import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.OpenAPIDefinition;
...@@ -36,7 +37,7 @@ import java.util.Base64; ...@@ -36,7 +37,7 @@ import java.util.Base64;
@Consumes({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON + "; charset=UTF-8"}) @Produces({MediaType.APPLICATION_JSON + "; charset=UTF-8"})
@OpenAPIDefinition( @OpenAPIDefinition(
info = @Info(title = "Event information for application")) info = @Info(title = "Event information for application"))
public class EventInfoV1 { public class EventInfoV1 {
private static final Logger logger = LoggerFactory.getLogger(EventInfoV1.class); private static final Logger logger = LoggerFactory.getLogger(EventInfoV1.class);
...@@ -52,6 +53,9 @@ public class EventInfoV1 { ...@@ -52,6 +53,9 @@ public class EventInfoV1 {
@EJB @EJB
private EventBeanLocal eventBean; private EventBeanLocal eventBean;
@EJB
private AuthHelperBeanLocal authHelperBean;
@GET @GET
@Path("/current") @Path("/current")
public Response getCurrentEventInfo() { public Response getCurrentEventInfo() {
...@@ -63,34 +67,52 @@ public class EventInfoV1 { ...@@ -63,34 +67,52 @@ public class EventInfoV1 {
public Response getEventsForUser() { public Response getEventsForUser() {
try { try {
String authHeader = servletRequest.getHeader(AUTH_HEADER); if (!permissionBean.isLoggedIn()) {
if (authHeader == null || !authHeader.startsWith(AUTH_PREFIX)) {
return Response.status(Response.Status.FORBIDDEN).entity("No basic auth provided").build();
}
String authStr = new String(Base64.getDecoder().decode(authHeader.substring(AUTH_PREFIX.length())));
String[] splitAuth = authStr.split(":", 2);
Principal principal = servletRequest.getUserPrincipal(); AuthHelperBeanLocal.RestAuthResponse authCredentials = authHelperBean.parseRestAuthCredentials(servletRequest);
// ensure logged out user String password;
if (principal != null && principal.getName() != null) { String username;
servletRequest.logout(); if (authCredentials != null) {
} username = authCredentials.username;
String domain = servletRequest.getHeader("host"); password = authCredentials.password;
} else {
String authHeader = servletRequest.getHeader(AUTH_HEADER);
if (authHeader == null || !authHeader.startsWith(AUTH_PREFIX)) {
return Response.status(Response.Status.FORBIDDEN).entity("No basic auth provided").build();
}
String authStr = new String(Base64.getDecoder().decode(authHeader.substring(AUTH_PREFIX.length())));
String[] splitAuth = authStr.split(":", 2);
servletRequest.getSession(true); Principal principal = servletRequest.getUserPrincipal();
servletRequest.login(splitAuth[0] + "@" + domain, splitAuth[1]); // ensure logged out user
if (principal != null && principal.getName() != null) {
servletRequest.logout();
}
String domain = authHelperBean.parseHostname(servletRequest);
username = splitAuth[0] + "@" + domain;
password = splitAuth[1];
}
servletRequest.getSession(true);
servletRequest.login(username, password);
}
return Response.ok(PojoUtils.parseEvents(eventBean.findAllEventsForCurrentUser())).build(); return Response.ok(PojoUtils.parseEvents(eventBean.findAllEventsForCurrentUser())).build();
} catch (ServletException e) { } catch (ServletException e) {
logger.warn("Error logging in while creating ApiApplication instance"); logger.warn("Error logging in while fetching events", e);
return Response.serverError().entity(e.getCause()).build(); return Response.serverError().entity(e.getCause()).build();
} }
} }
@GET @GET
@Path("/listevents/") @Path("/listevents/")
@Operation(description = "Get all events for current user", responses = { @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = EventRoot.class))) }) @Operation(description = "Get all events for current user", responses = {@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = EventRoot.class)))})
public Response getEventsForCurrentUser() { public Response getEventsForCurrentUser() {
if (permissionBean.getCurrentUser().isAnonymous()) { if (permissionBean.getCurrentUser().isAnonymous()) {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!