From 441c660af706fd3c6d0e06b36b8f25a808fcdf5f Mon Sep 17 00:00:00 2001
From: Roberto Sánchez <roberto.sanchez@curisit.net>
Date: Fri, 17 Jan 2014 17:35:50 +0000
Subject: [PATCH] #396 feature - Added security management methods for REST API
---
securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java | 64 ++++++++--
securis/src/main/java/net/curisit/securis/utils/TokenHelper.java | 22 +++
securis/src/main/java/net/curisit/securis/services/UserResource.java | 32 +++++
securis/src/main/java/net/curisit/securis/utils/CacheTTL.java | 125 ++++++++++++++++++++
securis/src/main/java/net/curisit/securis/services/Securable.java | 12 +
securis/src/main/java/net/curisit/securis/services/BasicServices.java | 70 +++++++++++
securis/src/main/resources/static/js/main.js | 17 ++
7 files changed, 320 insertions(+), 22 deletions(-)
diff --git a/securis/src/main/java/net/curisit/securis/services/BasicServices.java b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
index 38addaa..ef66c32 100644
--- a/securis/src/main/java/net/curisit/securis/services/BasicServices.java
+++ b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
@@ -6,14 +6,23 @@
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
+
+import net.curisit.integrity.commons.Utils;
+import net.curisit.securis.db.User;
+import net.curisit.securis.utils.TokenHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +37,9 @@
public class BasicServices {
private static final Logger log = LoggerFactory.getLogger(BasicServices.class);
+
+ @Inject
+ TokenHelper tokenHelper;
@Inject
public BasicServices() {
@@ -52,4 +64,62 @@
return Response.seeOther(uri).build();
}
+ @POST
+ @Path("/login")
+ @Produces(
+ { MediaType.APPLICATION_JSON })
+ public Response login(@FormParam("username") String user, @FormParam("password") String password, @Context HttpServletRequest request) {
+ log.info("index session: " + request.getSession());
+ log.info("user: {}, pass: {}", user, password);
+ log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
+
+ if ("no".equals(password))
+ return Response.status(Status.UNAUTHORIZED).build();
+ String tokenAuth = tokenHelper.generateToken(user);
+ return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
+ }
+
+ /**
+ * Check if current token is valid
+ *
+ * @param user
+ * @param password
+ * @param request
+ * @return
+ */
+ @GET
+ @Securable(roles = User.Rol.ADMIN)
+ @Path("/check")
+ @Produces(
+ { MediaType.APPLICATION_JSON })
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+ if (token == null)
+ token = token2;
+ if (token == null)
+ return Response.status(Status.FORBIDDEN).build();
+ boolean valid = tokenHelper.isTokenValid(token);
+ if (!valid)
+ return Response.status(Status.UNAUTHORIZED).build();
+
+ // log.info("Token : " + token);
+ String user = tokenHelper.extractUserFromToken(token);
+ // log.info("Token user: " + user);
+ Date date = tokenHelper.extractDateCreationFromToken(token);
+ // log.info("Token date: " + date);
+
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
+ }
+
+ @GET
+ @POST
+ @Path("/logout")
+ @Produces(
+ { MediaType.APPLICATION_JSON })
+ public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ if (token == null)
+ Response.status(Status.BAD_REQUEST).build();
+ String user = tokenHelper.extractUserFromToken(token);
+ log.info("User {} has logged out", user);
+ return Response.ok().build();
+ }
}
diff --git a/securis/src/main/java/net/curisit/securis/services/Securable.java b/securis/src/main/java/net/curisit/securis/services/Securable.java
index cad8f57..6fcb8e3 100644
--- a/securis/src/main/java/net/curisit/securis/services/Securable.java
+++ b/securis/src/main/java/net/curisit/securis/services/Securable.java
@@ -5,8 +5,18 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import net.curisit.securis.utils.TokenHelper;
+
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Securable {
- String header() default "session-token";
+ /**
+ * Name of header parameter with the auth token to validate
+ */
+ String header() default TokenHelper.TOKEN_HEADER_PÀRAM;
+
+ /**
+ * Bit mask with the rol or roles necessary to access the method
+ */
+ int roles() default 0;
}
\ No newline at end of file
diff --git a/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java b/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
index 16e2444..0516435 100644
--- a/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
@@ -2,11 +2,20 @@
import java.io.IOException;
import java.lang.reflect.Method;
+import java.util.List;
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.Provider;
+
+import net.curisit.securis.db.User;
+import net.curisit.securis.utils.CacheTTL;
+import net.curisit.securis.utils.TokenHelper;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.slf4j.Logger;
@@ -17,33 +26,60 @@
private static final Logger log = LoggerFactory.getLogger(SecurityInterceptor.class);
+ @Inject
+ private TokenHelper tokenHelper;
+
@Context
private HttpServletRequest servletRequest;
+
+ @Inject
+ CacheTTL cache;
+
+ @Inject
+ com.google.inject.Provider<EntityManager> emProvider;
@Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
log.info("filter using REST interceptor, method: {}", containerRequestContext.getMethod());
+
log.info("filter using REST interceptor, ResourceMethodInvoker: {}", containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker"));
ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
Method method = methodInvoker.getMethod();
if (!method.isAnnotationPresent(Securable.class))
return;
+ String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
+ if (token == null || !tokenHelper.isTokenValid(token))
+ containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
+ Securable sec = method.getAnnotation(Securable.class);
+
+ // If roles == 0 we only need to validate the token
+ if (sec.roles() != 0) {
+ String username = tokenHelper.extractUserFromToken(token);
+ int userRoles = getUserRoles(username);
+ if ((sec.roles() & userRoles) == 0) {
+ log.info("User {} has no necessary role to access url: {}", username, servletRequest.getPathInfo());
+ containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
+ }
+ }
}
- // @Override
- // public ServerResponse preProcess(HttpRequest httpRequest, ResourceMethod resourceMethod) throws Failure, WebApplicationException {
- //
- // Securable securable = resourceMethod.getMethod().getAnnotation(Securable.class);
- // String headerValue = servletRequest.getHeader(securable.header());
- //
- // if (headerValue == null) {
- // return (ServerResponse) Response.status(Status.BAD_REQUEST).entity("Invalid Session").build();
- // } else {
- // // Validatation logic goes here
- // }
- //
- // return null;
- // }
+ private int getUserRoles(String username) {
+ Integer userRoles = cache.get("roles_" + username, Integer.class);
+ if (userRoles == null) {
+ EntityManager em = emProvider.get();
+ User user = em.find(User.class, username);
+ if (user != null) {
+ userRoles = 0;
+ List<Integer> roles = user.getRoles();
+ for (Integer rol : roles) {
+ userRoles += rol;
+ }
+ // We store user roles in cache only for one hour
+ cache.set("roles_" + username, userRoles, 3600);
+ }
+ }
+ return userRoles == null ? 0 : userRoles.intValue();
+ }
}
diff --git a/securis/src/main/java/net/curisit/securis/services/UserResource.java b/securis/src/main/java/net/curisit/securis/services/UserResource.java
index 0b5e208..2bc90ee 100644
--- a/securis/src/main/java/net/curisit/securis/services/UserResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/UserResource.java
@@ -19,6 +19,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -206,13 +207,42 @@
log.info("user: {}, pass: {}", user, password);
log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
- request.getSession().setAttribute("username", user);
if ("no".equals(password))
return Response.status(Status.UNAUTHORIZED).build();
String tokenAuth = tokenHelper.generateToken(user);
return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
}
+ /**
+ * Check if current token is valid
+ *
+ * @param user
+ * @param password
+ * @param request
+ * @return
+ */
+ @POST
+ @Path("/check")
+ @Produces(
+ { MediaType.APPLICATION_JSON })
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+ if (token == null)
+ token = token2;
+ if (token == null)
+ return Response.status(Status.FORBIDDEN).build();
+
+ log.info("Token : " + token);
+ String user = tokenHelper.extractUserFromToken(token);
+ log.info("Token user: " + user);
+ Date date = tokenHelper.extractDateCreationFromToken(token);
+ log.info("Token date: " + date);
+ boolean valid = tokenHelper.isTokenValid(token);
+
+ log.info("Is Token valid: " + valid);
+
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
+ }
+
@GET
@Path("/logout")
@Produces(
diff --git a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
new file mode 100644
index 0000000..51a59e4
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
@@ -0,0 +1,125 @@
+package net.curisit.securis.utils;
+
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Cache implementation with TTL (time To Live) The objects are removed from cache when TTL is reached.
+ *
+ * @author roberto <roberto.sanchez@curisit.net>
+ */
+@Singleton
+public class CacheTTL {
+
+ private static final Logger log = LoggerFactory.getLogger(CacheTTL.class);
+
+ /**
+ * Period before token expires, set in seconds.
+ */
+ private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
+
+ private Map<String, CachedObject> data = new Hashtable<>();
+
+ private Thread cleaningThread = null;
+
+ @Inject
+ public CacheTTL() {
+ cleaningThread = new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ while (CacheTTL.this.data != null) {
+ try {
+ // We check for expired object every 60 seconds
+ Thread.sleep(60 * 1000);
+ } catch (InterruptedException e) {
+ log.error("Exiting from Cache Thread");
+ data.clear();
+ return;
+ }
+ // log.info("Cheking expired objects " + new Date());
+ Date now = new Date();
+ for (String key : CacheTTL.this.data.keySet()) {
+ CachedObject co = CacheTTL.this.data.get(key);
+ if (now.after(co.getExpireAt())) {
+ CacheTTL.this.data.remove(key);
+ }
+ }
+ }
+ }
+ });
+ cleaningThread.start();
+ }
+
+ /**
+ *
+ * @param key
+ * @param obj
+ * @param ttl
+ * Time To Live in seconds
+ */
+ public void set(String key, Object obj, int ttl) {
+ Date expirationDate = new Date(new Date().getTime() + ttl * 1000);
+ data.put(key, new CachedObject(expirationDate, obj));
+ }
+
+ public void set(String key, Object obj) {
+ set(key, obj, DEFAULT_CACHE_DURATION);
+ }
+
+ public Object get(String key) {
+ CachedObject co = data.get(key);
+ return co == null ? null : co.getObject();
+ }
+
+ public <T> T get(String key, Class<T> type) {
+ CachedObject co = data.get(key);
+ return co == null ? null : co.getObject(type);
+ }
+
+ public <T> T remove(String key, Class<T> type) {
+ CachedObject co = data.remove(key);
+ return co == null ? null : co.getObject(type);
+ }
+
+ public Object remove(String key) {
+ CachedObject co = data.remove(key);
+ return co == null ? null : co.getObject();
+ }
+
+ public void clear() {
+ data.clear();
+ }
+
+ private class CachedObject {
+ Date expireAt;
+ Object object;
+
+ public CachedObject(Date date, Object obj) {
+ expireAt = date;
+ object = obj;
+ }
+
+ public Date getExpireAt() {
+ return expireAt;
+ }
+
+ public Object getObject() {
+ return object;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T getObject(Class<T> type) {
+ return (T) object;
+ }
+
+ }
+
+}
diff --git a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
index df247d0..b7d82de 100644
--- a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
@@ -78,10 +78,12 @@
* @param token
* @return
*/
- public boolean validateToken(String token) {
+ public boolean isTokenValid(String token) {
try {
String tokenDecoded = new String(Base64.decode(token));
String[] parts = StringUtils.split(tokenDecoded, ' ');
+ if (parts == null || parts.length < 3)
+ return false;
String secret = parts[0];
String user = parts[1];
Date date = Utils.toDateFromIso(parts[2]);
@@ -101,8 +103,24 @@
try {
String tokenDecoded = new String(Base64.decode(token));
String[] parts = StringUtils.split(tokenDecoded, ' ');
+ if (parts == null || parts.length < 3)
+ return null;
String user = parts[1];
return user;
+ } catch (IOException e) {
+ log.error("Error decoding Base64 token", e);
+ }
+ return null;
+ }
+
+ public Date extractDateCreationFromToken(String token) {
+ try {
+ String tokenDecoded = new String(Base64.decode(token));
+ String[] parts = StringUtils.split(tokenDecoded, ' ');
+ if (parts == null || parts.length < 3)
+ return null;
+ Date date = Utils.toDateFromIso(parts[2]);
+ return date;
} catch (IOException e) {
log.error("Error decoding Base64 token", e);
}
@@ -114,6 +132,6 @@
String token = th.generateToken("pepe");
System.out.println("Token: " + token);
System.out.println("Token: " + new String(Base64.decode(token)));
- System.out.println("Valid Token: " + th.validateToken(token));
+ System.out.println("Valid Token: " + th.isTokenValid(token));
}
}
diff --git a/securis/src/main/resources/static/js/main.js b/securis/src/main/resources/static/js/main.js
index 14521b6..58a7a9b 100644
--- a/securis/src/main/resources/static/js/main.js
+++ b/securis/src/main/resources/static/js/main.js
@@ -67,11 +67,20 @@
m.controller('MainCtrl', ['$scope', '$http', '$location', '$L', '$store',
function($scope, $http, $location, $L, $store) {
+ $location.path('/login');
if ($store.get('token') != null) {
- $http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token');
- $location.path('/licenses');
- } else {
- $location.path('/login');
+
+ $http.get('/check', {
+ headers: {
+ 'X-SECURIS-TOKEN': $store.get('token')
+ }
+ }).success(function(data) {
+ if (data.valid) {
+ $http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token');
+ $location.path('/licenses');
+ $store.set('user', data.user);
+ }
+ });
}
$scope.logout = function() {
--
Gitblit v1.3.2