Closes #106: Header base authentication

This commit is contained in:
jendib 2016-05-16 21:07:01 +02:00
parent ce0678784b
commit 67a4dc63ca
No known key found for this signature in database
GPG Key ID: 06EE7F699579166F
8 changed files with 302 additions and 152 deletions

View File

@ -11,7 +11,7 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
/** /**
* Utility class for tags. * Utility class for spannable.
* *
* @author bgamard. * @author bgamard.
*/ */

View File

@ -0,0 +1,47 @@
package com.sismics.util.filter;
import com.google.common.base.Strings;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.jpa.User;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
/**
* A header-based security filter that authenticates an user using the "X-Authenticated-User" request header as the user ID.
* This filter is intended to be used in conjunction with an external authenticating proxy.
*
* @author pacien
*/
public class HeaderBasedSecurityFilter extends SecurityFilter {
/**
* Authentication header.
*/
public static final String AUTHENTICATED_USER_HEADER = "X-Authenticated-User";
/**
* True if this authentication method is enabled.
*/
private boolean enabled;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
enabled = Boolean.parseBoolean(filterConfig.getInitParameter("enabled"))
|| Boolean.parseBoolean(System.getProperty("docs.header_authentication"));
}
@Override
protected User authenticate(HttpServletRequest request) {
if (!enabled) {
return null;
}
String username = request.getHeader(AUTHENTICATED_USER_HEADER);
if (Strings.isNullOrEmpty(username)) {
return null;
}
return new UserDao().getActiveByUsername(username);
}
}

View File

@ -0,0 +1,145 @@
package com.sismics.util.filter;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.GroupDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.security.AnonymousPrincipal;
import com.sismics.security.UserPrincipal;
import jersey.repackaged.com.google.common.collect.Sets;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An abstract security filter for user authentication, that injects corresponding users into the request.
* Successfully authenticated users are injected as UserPrincipal, or as AnonymousPrincipal otherwise.
* If an user has already been authenticated for the request, no further authentication attempt is made.
*
* @author pacien
* @author jtremeaux
*/
public abstract class SecurityFilter implements Filter {
/**
* Name of the attribute containing the principal.
*/
public static final String PRINCIPAL_ATTRIBUTE = "principal";
/**
* Logger.
*/
static final Logger LOG = LoggerFactory.getLogger(SecurityFilter.class);
/**
* Returns true if the supplied request has an UserPrincipal.
*
* @param request HTTP request
* @return True if the supplied request has an UserPrincipal
*/
private boolean hasIdentifiedUser(HttpServletRequest request) {
return request.getAttribute(PRINCIPAL_ATTRIBUTE) instanceof UserPrincipal;
}
/**
* Injects the given user into the request, with the appropriate authentication state.
*
* @param request HTTP request
* @param user nullable User to inject
*/
private void injectUser(HttpServletRequest request, User user) {
// Check if the user is still valid
if (user != null && user.getDeleteDate() == null) {
injectAuthenticatedUser(request, user);
} else {
injectAnonymousUser(request);
}
}
/**
* Inject an authenticated user into the request attributes.
*
* @param request HTTP request
* @param user User to inject
*/
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
// Add groups
GroupDao groupDao = new GroupDao();
Set<String> groupRoleIdSet = new HashSet<>();
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
.setUserId(user.getId())
.setRecursive(true), null);
Set<String> groupIdSet = Sets.newHashSet();
for (GroupDto groupDto : groupDtoList) {
groupIdSet.add(groupDto.getId());
if (groupDto.getRoleId() != null) {
groupRoleIdSet.add(groupDto.getRoleId());
}
}
userPrincipal.setGroupIdSet(groupIdSet);
// Add base functions
groupRoleIdSet.add(user.getRoleId());
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
userPrincipal.setBaseFunctionSet(baseFunctionSet);
// Add email
userPrincipal.setEmail(user.getEmail());
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
}
/**
* Inject an anonymous user into the request attributes.
*
* @param request HTTP request
*/
private void injectAnonymousUser(HttpServletRequest request) {
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (!hasIdentifiedUser(request)) {
User user = authenticate(request);
injectUser(request, user);
}
filterChain.doFilter(request, response);
}
/**
* Authenticates an user from the given request parameters.
*
* @param request HTTP request
* @return nullable User
*/
protected abstract User authenticate(HttpServletRequest request);
}

View File

@ -1,38 +1,14 @@
package com.sismics.util.filter; package com.sismics.util.filter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao;
import com.sismics.docs.core.dao.jpa.GroupDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
import com.sismics.docs.core.model.jpa.AuthenticationToken; import com.sismics.docs.core.model.jpa.AuthenticationToken;
import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.model.jpa.User;
import com.sismics.security.AnonymousPrincipal;
import com.sismics.security.UserPrincipal;
import jersey.repackaged.com.google.common.collect.Sets; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
import java.util.Date;
/** /**
* This filter is used to authenticate the user having an active session via an authentication token stored in database. * This filter is used to authenticate the user having an active session via an authentication token stored in database.
@ -42,22 +18,12 @@ import jersey.repackaged.com.google.common.collect.Sets;
* *
* @author jtremeaux * @author jtremeaux
*/ */
public class TokenBasedSecurityFilter implements Filter { public class TokenBasedSecurityFilter extends SecurityFilter {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class);
/** /**
* Name of the cookie used to store the authentication token. * Name of the cookie used to store the authentication token.
*/ */
public static final String COOKIE_NAME = "auth_token"; public static final String COOKIE_NAME = "auth_token";
/**
* Name of the attribute containing the principal.
*/
public static final String PRINCIPAL_ATTRIBUTE = "principal";
/** /**
* Lifetime of the authentication token in seconds, since login. * Lifetime of the authentication token in seconds, since login.
*/ */
@ -66,68 +32,40 @@ public class TokenBasedSecurityFilter implements Filter {
/** /**
* Lifetime of the authentication token in seconds, since last connection. * Lifetime of the authentication token in seconds, since last connection.
*/ */
public static final int TOKEN_SESSION_LIFETIME = 3600 * 24; private static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
@Override /**
public void destroy() { * Extracts and returns an authentication token from a cookie list.
// NOP *
} * @param cookies Cookie list
* @return nullable auth token
@Override */
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { private String extractAuthToken(Cookie[] cookies) {
// Get the value of the client authentication token if (cookies != null) {
HttpServletRequest request = (HttpServletRequest) req; for (Cookie cookie : cookies) {
String authToken = null; if (COOKIE_NAME.equals(cookie.getName()) && !cookie.getValue().isEmpty()) {
if (request.getCookies() != null) { return cookie.getValue();
for (Cookie cookie : request.getCookies()) {
if (COOKIE_NAME.equals(cookie.getName())) {
authToken = cookie.getValue();
} }
} }
} }
// Get the corresponding server token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
AuthenticationToken authenticationToken = null;
if (authToken != null) {
authenticationToken = authenticationTokenDao.get(authToken);
}
if (authenticationToken == null) {
injectAnonymousUser(request);
} else {
// Check if the token is still valid
if (isTokenExpired(authenticationToken)) {
try {
injectAnonymousUser(request);
// Destroy the expired token return null;
authenticationTokenDao.delete(authToken);
} catch (Exception e) {
if (log.isErrorEnabled()) {
log.error(MessageFormat.format("Error deleting authentication token {0} ", authToken), e);
}
}
} else {
// Check if the user is still valid
UserDao userDao = new UserDao();
User user = userDao.getById(authenticationToken.getUserId());
if (user != null && user.getDeleteDate() == null) {
injectAuthenticatedUser(request, user);
} else {
injectAnonymousUser(request);
}
}
}
filterChain.doFilter(request, response);
} }
/**
* Deletes an expired authentication token.
*
* @param authTokenID auth token ID
*/
private void handleExpiredToken(AuthenticationTokenDao dao, String authTokenID) {
try {
dao.delete(authTokenID);
} catch (Exception e) {
if (LOG.isErrorEnabled())
LOG.error(MessageFormat.format("Error deleting authentication token {0} ", authTokenID), e);
}
}
/** /**
* Returns true if the token is expired. * Returns true if the token is expired.
* *
@ -146,51 +84,27 @@ public class TokenBasedSecurityFilter implements Filter {
} }
} }
/** @Override
* Inject an authenticated user into the request attributes. protected User authenticate(HttpServletRequest request) {
* // Get the value of the client authentication token
* @param request HTTP request String authTokenId = extractAuthToken(request.getCookies());
* @param user User to inject if (authTokenId == null) {
*/ return null;
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
// Add groups
GroupDao groupDao = new GroupDao();
Set<String> groupRoleIdSet = new HashSet<>();
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
.setUserId(user.getId())
.setRecursive(true), null);
Set<String> groupIdSet = Sets.newHashSet();
for (GroupDto groupDto : groupDtoList) {
groupIdSet.add(groupDto.getId());
if (groupDto.getRoleId() != null) {
groupRoleIdSet.add(groupDto.getRoleId());
}
} }
userPrincipal.setGroupIdSet(groupIdSet);
// Add base functions
groupRoleIdSet.add(user.getRoleId());
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
userPrincipal.setBaseFunctionSet(baseFunctionSet);
// Add email
userPrincipal.setEmail(user.getEmail());
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
}
/** // Get the corresponding server token
* Inject an anonymous user into the request attributes. AuthenticationTokenDao authTokenDao = new AuthenticationTokenDao();
* AuthenticationToken authToken = authTokenDao.get(authTokenId);
* @param request HTTP request if (authToken == null) {
*/ return null;
private void injectAnonymousUser(HttpServletRequest request) { }
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal); if (isTokenExpired(authToken)) {
handleExpiredToken(authTokenDao, authTokenId);
return null;
}
authTokenDao.updateLastConnectionDate(authToken.getId());
return new UserDao().getById(authToken.getUserId());
} }
} }

View File

@ -5,6 +5,7 @@ import java.net.URI;
import javax.ws.rs.core.Application; import javax.ws.rs.core.Application;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.servlet.ServletRegistration; import org.glassfish.grizzly.servlet.ServletRegistration;
import org.glassfish.grizzly.servlet.WebappContext; import org.glassfish.grizzly.servlet.WebappContext;
@ -62,7 +63,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
System.setProperty("docs.header_authentication", "true");
clientUtil = new ClientUtil(target()); clientUtil = new ClientUtil(target());
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort()); httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
@ -71,6 +73,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
.addMappingForUrlPatterns(null, "/*"); .addMappingForUrlPatterns(null, "/*");
context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class) context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*"); .addMappingForUrlPatterns(null, "/*");
context.addFilter("headerBasedSecurityFilter", HeaderBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*");
ServletRegistration reg = context.addServlet("jerseyServlet", ServletContainer.class); ServletRegistration reg = context.addServlet("jerseyServlet", ServletContainer.class);
reg.setInitParameter("jersey.config.server.provider.packages", "com.sismics.docs.rest.resource"); reg.setInitParameter("jersey.config.server.provider.packages", "com.sismics.docs.rest.resource");
reg.setInitParameter("jersey.config.server.provider.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature"); reg.setInitParameter("jersey.config.server.provider.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature");

View File

@ -1,19 +1,18 @@
package com.sismics.docs.rest.resource; package com.sismics.docs.rest.resource;
import java.security.Principal;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.security.IPrincipal; import com.sismics.security.IPrincipal;
import com.sismics.security.UserPrincipal; import com.sismics.security.UserPrincipal;
import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.filter.SecurityFilter;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import java.security.Principal;
import java.util.List;
import java.util.Set;
/** /**
* Base class of REST resources. * Base class of REST resources.
@ -67,7 +66,7 @@ public abstract class BaseResource {
* @return True if the user is authenticated and not anonymous * @return True if the user is authenticated and not anonymous
*/ */
protected boolean authenticate() { protected boolean authenticate() {
Principal principal = (Principal) request.getAttribute(TokenBasedSecurityFilter.PRINCIPAL_ATTRIBUTE); Principal principal = (Principal) request.getAttribute(SecurityFilter.PRINCIPAL_ATTRIBUTE);
if (principal != null && principal instanceof IPrincipal) { if (principal != null && principal instanceof IPrincipal) {
this.principal = (IPrincipal) principal; this.principal = (IPrincipal) principal;
return !this.principal.isAnonymous(); return !this.principal.isAnonymous();

View File

@ -26,18 +26,33 @@
<url-pattern>*.jsp</url-pattern> <url-pattern>*.jsp</url-pattern>
</filter-mapping> </filter-mapping>
<!-- This filter is used to secure URLs --> <!-- These filters are used to secure URLs -->
<filter> <filter>
<filter-name>tokenBasedSecurityFilter</filter-name> <filter-name>tokenBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.TokenBasedSecurityFilter</filter-class> <filter-class>com.sismics.util.filter.TokenBasedSecurityFilter</filter-class>
<async-supported>true</async-supported> <async-supported>true</async-supported>
</filter> </filter>
<filter>
<filter-name>headerBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.HeaderBasedSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>enabled</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping> <filter-mapping>
<filter-name>tokenBasedSecurityFilter</filter-name> <filter-name>tokenBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern> <url-pattern>/api/*</url-pattern>
</filter-mapping> </filter-mapping>
<filter-mapping>
<filter-name>headerBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<!-- Jersey --> <!-- Jersey -->
<servlet> <servlet>
<servlet-name>JerseyServlet</servlet-name> <servlet-name>JerseyServlet</servlet-name>

View File

@ -6,6 +6,7 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.junit.Assert; import org.junit.Assert;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
@ -28,7 +29,7 @@ public class TestSecurity extends BaseJerseyTest {
clientUtil.createUser("testsecurity"); clientUtil.createUser("testsecurity");
// Changes a user's email KO : the user is not connected // Changes a user's email KO : the user is not connected
Response response = target().path("/user/update").request() Response response = target().path("/user").request()
.post(Entity.form(new Form().param("email", "testsecurity2@docs.com"))); .post(Entity.form(new Form().param("email", "testsecurity2@docs.com")));
Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus()));
JsonObject json = response.readEntity(JsonObject.class); JsonObject json = response.readEntity(JsonObject.class);
@ -73,4 +74,29 @@ public class TestSecurity extends BaseJerseyTest {
// User testsecurity logs out // User testsecurity logs out
clientUtil.logout(testSecurityToken); clientUtil.logout(testSecurityToken);
} }
@Test
public void testHeaderBasedAuthentication() {
clientUtil.createUser("header_auth_test");
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
.path("/user/session")
.request()
.get()
.getStatus());
Assert.assertEquals(Status.OK.getStatusCode(), target()
.path("/user/session")
.request()
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "header_auth_test")
.get()
.getStatus());
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
.path("/user/session")
.request()
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "idontexist")
.get()
.getStatus());
}
} }