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;
/**
* Utility class for tags.
* Utility class for spannable.
*
* @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;
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.GroupDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
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.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.
@ -42,22 +18,12 @@ import jersey.repackaged.com.google.common.collect.Sets;
*
* @author jtremeaux
*/
public class TokenBasedSecurityFilter implements Filter {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class);
public class TokenBasedSecurityFilter extends SecurityFilter {
/**
* Name of the cookie used to store the authentication 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.
*/
@ -66,68 +32,40 @@ public class TokenBasedSecurityFilter implements Filter {
/**
* Lifetime of the authentication token in seconds, since last connection.
*/
public static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
private static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// Get the value of the client authentication token
HttpServletRequest request = (HttpServletRequest) req;
String authToken = null;
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (COOKIE_NAME.equals(cookie.getName())) {
authToken = cookie.getValue();
/**
* Extracts and returns an authentication token from a cookie list.
*
* @param cookies Cookie list
* @return nullable auth token
*/
private String extractAuthToken(Cookie[] cookies) {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (COOKIE_NAME.equals(cookie.getName()) && !cookie.getValue().isEmpty()) {
return 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
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);
return null;
}
/**
* 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.
*
@ -146,51 +84,27 @@ public class TokenBasedSecurityFilter implements Filter {
}
}
/**
* 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());
}
@Override
protected User authenticate(HttpServletRequest request) {
// Get the value of the client authentication token
String authTokenId = extractAuthToken(request.getCookies());
if (authTokenId == null) {
return null;
}
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));
// Get the corresponding server token
AuthenticationTokenDao authTokenDao = new AuthenticationTokenDao();
AuthenticationToken authToken = authTokenDao.get(authTokenId);
if (authToken == null) {
return null;
}
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.UriBuilder;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.servlet.ServletRegistration;
import org.glassfish.grizzly.servlet.WebappContext;
@ -62,7 +63,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
@Before
public void setUp() throws Exception {
super.setUp();
System.setProperty("docs.header_authentication", "true");
clientUtil = new ClientUtil(target());
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
@ -71,6 +73,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
.addMappingForUrlPatterns(null, "/*");
context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*");
context.addFilter("headerBasedSecurityFilter", HeaderBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*");
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.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature");

View File

@ -1,19 +1,18 @@
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.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.security.IPrincipal;
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.
@ -67,7 +66,7 @@ public abstract class BaseResource {
* @return True if the user is authenticated and not anonymous
*/
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) {
this.principal = (IPrincipal) principal;
return !this.principal.isAnonymous();

View File

@ -26,18 +26,33 @@
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- This filter is used to secure URLs -->
<!-- These filters are used to secure URLs -->
<filter>
<filter-name>tokenBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.TokenBasedSecurityFilter</filter-class>
<async-supported>true</async-supported>
</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-name>tokenBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>headerBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<!-- Jersey -->
<servlet>
<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.Status;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.junit.Assert;
import org.apache.commons.lang.StringUtils;
@ -28,7 +29,7 @@ public class TestSecurity extends BaseJerseyTest {
clientUtil.createUser("testsecurity");
// 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")));
Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus()));
JsonObject json = response.readEntity(JsonObject.class);
@ -73,4 +74,29 @@ public class TestSecurity extends BaseJerseyTest {
// User testsecurity logs out
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());
}
}