Roberto Sánchez
2014-01-17 441c660af706fd3c6d0e06b36b8f25a808fcdf5f
#396 feature - Added security management methods for REST API
1 files added
6 files modified
changed files
securis/src/main/java/net/curisit/securis/services/BasicServices.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/Securable.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/UserResource.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/utils/CacheTTL.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/utils/TokenHelper.java patch | view | blame | history
securis/src/main/resources/static/js/main.js patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/BasicServices.java
....@@ -6,14 +6,23 @@
66 import javax.inject.Inject;
77 import javax.inject.Singleton;
88 import javax.servlet.http.HttpServletRequest;
9
+import javax.ws.rs.FormParam;
910 import javax.ws.rs.GET;
11
+import javax.ws.rs.HeaderParam;
12
+import javax.ws.rs.POST;
1013 import javax.ws.rs.Path;
1114 import javax.ws.rs.PathParam;
1215 import javax.ws.rs.Produces;
16
+import javax.ws.rs.QueryParam;
1317 import javax.ws.rs.core.Context;
1418 import javax.ws.rs.core.MediaType;
1519 import javax.ws.rs.core.Response;
20
+import javax.ws.rs.core.Response.Status;
1621 import javax.ws.rs.core.UriBuilder;
22
+
23
+import net.curisit.integrity.commons.Utils;
24
+import net.curisit.securis.db.User;
25
+import net.curisit.securis.utils.TokenHelper;
1726
1827 import org.slf4j.Logger;
1928 import org.slf4j.LoggerFactory;
....@@ -28,6 +37,9 @@
2837 public class BasicServices {
2938
3039 private static final Logger log = LoggerFactory.getLogger(BasicServices.class);
40
+
41
+ @Inject
42
+ TokenHelper tokenHelper;
3143
3244 @Inject
3345 public BasicServices() {
....@@ -52,4 +64,62 @@
5264 return Response.seeOther(uri).build();
5365 }
5466
67
+ @POST
68
+ @Path("/login")
69
+ @Produces(
70
+ { MediaType.APPLICATION_JSON })
71
+ public Response login(@FormParam("username") String user, @FormParam("password") String password, @Context HttpServletRequest request) {
72
+ log.info("index session: " + request.getSession());
73
+ log.info("user: {}, pass: {}", user, password);
74
+ log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
75
+
76
+ if ("no".equals(password))
77
+ return Response.status(Status.UNAUTHORIZED).build();
78
+ String tokenAuth = tokenHelper.generateToken(user);
79
+ return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
80
+ }
81
+
82
+ /**
83
+ * Check if current token is valid
84
+ *
85
+ * @param user
86
+ * @param password
87
+ * @param request
88
+ * @return
89
+ */
90
+ @GET
91
+ @Securable(roles = User.Rol.ADMIN)
92
+ @Path("/check")
93
+ @Produces(
94
+ { MediaType.APPLICATION_JSON })
95
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
96
+ if (token == null)
97
+ token = token2;
98
+ if (token == null)
99
+ return Response.status(Status.FORBIDDEN).build();
100
+ boolean valid = tokenHelper.isTokenValid(token);
101
+ if (!valid)
102
+ return Response.status(Status.UNAUTHORIZED).build();
103
+
104
+ // log.info("Token : " + token);
105
+ String user = tokenHelper.extractUserFromToken(token);
106
+ // log.info("Token user: " + user);
107
+ Date date = tokenHelper.extractDateCreationFromToken(token);
108
+ // log.info("Token date: " + date);
109
+
110
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
111
+ }
112
+
113
+ @GET
114
+ @POST
115
+ @Path("/logout")
116
+ @Produces(
117
+ { MediaType.APPLICATION_JSON })
118
+ public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
119
+ if (token == null)
120
+ Response.status(Status.BAD_REQUEST).build();
121
+ String user = tokenHelper.extractUserFromToken(token);
122
+ log.info("User {} has logged out", user);
123
+ return Response.ok().build();
124
+ }
55125 }
securis/src/main/java/net/curisit/securis/services/Securable.java
....@@ -5,8 +5,18 @@
55 import java.lang.annotation.RetentionPolicy;
66 import java.lang.annotation.Target;
77
8
+import net.curisit.securis.utils.TokenHelper;
9
+
810 @Retention(RetentionPolicy.RUNTIME)
911 @Target(ElementType.METHOD)
1012 public @interface Securable {
11
- String header() default "session-token";
13
+ /**
14
+ * Name of header parameter with the auth token to validate
15
+ */
16
+ String header() default TokenHelper.TOKEN_HEADER_PÀRAM;
17
+
18
+ /**
19
+ * Bit mask with the rol or roles necessary to access the method
20
+ */
21
+ int roles() default 0;
1222 }
securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
....@@ -2,11 +2,20 @@
22
33 import java.io.IOException;
44 import java.lang.reflect.Method;
5
+import java.util.List;
56
7
+import javax.inject.Inject;
8
+import javax.persistence.EntityManager;
69 import javax.servlet.http.HttpServletRequest;
710 import javax.ws.rs.container.ContainerRequestContext;
811 import javax.ws.rs.core.Context;
12
+import javax.ws.rs.core.Response;
13
+import javax.ws.rs.core.Response.Status;
914 import javax.ws.rs.ext.Provider;
15
+
16
+import net.curisit.securis.db.User;
17
+import net.curisit.securis.utils.CacheTTL;
18
+import net.curisit.securis.utils.TokenHelper;
1019
1120 import org.jboss.resteasy.core.ResourceMethodInvoker;
1221 import org.slf4j.Logger;
....@@ -17,33 +26,60 @@
1726
1827 private static final Logger log = LoggerFactory.getLogger(SecurityInterceptor.class);
1928
29
+ @Inject
30
+ private TokenHelper tokenHelper;
31
+
2032 @Context
2133 private HttpServletRequest servletRequest;
34
+
35
+ @Inject
36
+ CacheTTL cache;
37
+
38
+ @Inject
39
+ com.google.inject.Provider<EntityManager> emProvider;
2240
2341 @Override
2442 public void filter(ContainerRequestContext containerRequestContext) throws IOException {
2543 log.info("filter using REST interceptor, method: {}", containerRequestContext.getMethod());
44
+
2645 log.info("filter using REST interceptor, ResourceMethodInvoker: {}", containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker"));
2746 ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
2847 Method method = methodInvoker.getMethod();
2948
3049 if (!method.isAnnotationPresent(Securable.class))
3150 return;
51
+ String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
52
+ if (token == null || !tokenHelper.isTokenValid(token))
53
+ containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
54
+ Securable sec = method.getAnnotation(Securable.class);
55
+
56
+ // If roles == 0 we only need to validate the token
57
+ if (sec.roles() != 0) {
58
+ String username = tokenHelper.extractUserFromToken(token);
59
+ int userRoles = getUserRoles(username);
60
+ if ((sec.roles() & userRoles) == 0) {
61
+ log.info("User {} has no necessary role to access url: {}", username, servletRequest.getPathInfo());
62
+ containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
63
+ }
64
+ }
3265 }
3366
34
- // @Override
35
- // public ServerResponse preProcess(HttpRequest httpRequest, ResourceMethod resourceMethod) throws Failure, WebApplicationException {
36
- //
37
- // Securable securable = resourceMethod.getMethod().getAnnotation(Securable.class);
38
- // String headerValue = servletRequest.getHeader(securable.header());
39
- //
40
- // if (headerValue == null) {
41
- // return (ServerResponse) Response.status(Status.BAD_REQUEST).entity("Invalid Session").build();
42
- // } else {
43
- // // Validatation logic goes here
44
- // }
45
- //
46
- // return null;
47
- // }
67
+ private int getUserRoles(String username) {
68
+ Integer userRoles = cache.get("roles_" + username, Integer.class);
69
+ if (userRoles == null) {
70
+ EntityManager em = emProvider.get();
71
+ User user = em.find(User.class, username);
72
+ if (user != null) {
73
+ userRoles = 0;
74
+ List<Integer> roles = user.getRoles();
75
+ for (Integer rol : roles) {
76
+ userRoles += rol;
77
+ }
78
+ // We store user roles in cache only for one hour
79
+ cache.set("roles_" + username, userRoles, 3600);
80
+ }
81
+ }
82
+ return userRoles == null ? 0 : userRoles.intValue();
83
+ }
4884
4985 }
securis/src/main/java/net/curisit/securis/services/UserResource.java
....@@ -19,6 +19,7 @@
1919 import javax.ws.rs.Path;
2020 import javax.ws.rs.PathParam;
2121 import javax.ws.rs.Produces;
22
+import javax.ws.rs.QueryParam;
2223 import javax.ws.rs.core.Context;
2324 import javax.ws.rs.core.MediaType;
2425 import javax.ws.rs.core.Response;
....@@ -206,13 +207,42 @@
206207 log.info("user: {}, pass: {}", user, password);
207208 log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
208209
209
- request.getSession().setAttribute("username", user);
210210 if ("no".equals(password))
211211 return Response.status(Status.UNAUTHORIZED).build();
212212 String tokenAuth = tokenHelper.generateToken(user);
213213 return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
214214 }
215215
216
+ /**
217
+ * Check if current token is valid
218
+ *
219
+ * @param user
220
+ * @param password
221
+ * @param request
222
+ * @return
223
+ */
224
+ @POST
225
+ @Path("/check")
226
+ @Produces(
227
+ { MediaType.APPLICATION_JSON })
228
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
229
+ if (token == null)
230
+ token = token2;
231
+ if (token == null)
232
+ return Response.status(Status.FORBIDDEN).build();
233
+
234
+ log.info("Token : " + token);
235
+ String user = tokenHelper.extractUserFromToken(token);
236
+ log.info("Token user: " + user);
237
+ Date date = tokenHelper.extractDateCreationFromToken(token);
238
+ log.info("Token date: " + date);
239
+ boolean valid = tokenHelper.isTokenValid(token);
240
+
241
+ log.info("Is Token valid: " + valid);
242
+
243
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
244
+ }
245
+
216246 @GET
217247 @Path("/logout")
218248 @Produces(
securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
....@@ -0,0 +1,125 @@
1
+package net.curisit.securis.utils;
2
+
3
+import java.util.Date;
4
+import java.util.Hashtable;
5
+import java.util.Map;
6
+
7
+import javax.inject.Inject;
8
+import javax.inject.Singleton;
9
+
10
+import org.slf4j.Logger;
11
+import org.slf4j.LoggerFactory;
12
+
13
+/**
14
+ * Cache implementation with TTL (time To Live) The objects are removed from cache when TTL is reached.
15
+ *
16
+ * @author roberto <roberto.sanchez@curisit.net>
17
+ */
18
+@Singleton
19
+public class CacheTTL {
20
+
21
+ private static final Logger log = LoggerFactory.getLogger(CacheTTL.class);
22
+
23
+ /**
24
+ * Period before token expires, set in seconds.
25
+ */
26
+ private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
27
+
28
+ private Map<String, CachedObject> data = new Hashtable<>();
29
+
30
+ private Thread cleaningThread = null;
31
+
32
+ @Inject
33
+ public CacheTTL() {
34
+ cleaningThread = new Thread(new Runnable() {
35
+
36
+ @Override
37
+ public void run() {
38
+ while (CacheTTL.this.data != null) {
39
+ try {
40
+ // We check for expired object every 60 seconds
41
+ Thread.sleep(60 * 1000);
42
+ } catch (InterruptedException e) {
43
+ log.error("Exiting from Cache Thread");
44
+ data.clear();
45
+ return;
46
+ }
47
+ // log.info("Cheking expired objects " + new Date());
48
+ Date now = new Date();
49
+ for (String key : CacheTTL.this.data.keySet()) {
50
+ CachedObject co = CacheTTL.this.data.get(key);
51
+ if (now.after(co.getExpireAt())) {
52
+ CacheTTL.this.data.remove(key);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ });
58
+ cleaningThread.start();
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param key
64
+ * @param obj
65
+ * @param ttl
66
+ * Time To Live in seconds
67
+ */
68
+ public void set(String key, Object obj, int ttl) {
69
+ Date expirationDate = new Date(new Date().getTime() + ttl * 1000);
70
+ data.put(key, new CachedObject(expirationDate, obj));
71
+ }
72
+
73
+ public void set(String key, Object obj) {
74
+ set(key, obj, DEFAULT_CACHE_DURATION);
75
+ }
76
+
77
+ public Object get(String key) {
78
+ CachedObject co = data.get(key);
79
+ return co == null ? null : co.getObject();
80
+ }
81
+
82
+ public <T> T get(String key, Class<T> type) {
83
+ CachedObject co = data.get(key);
84
+ return co == null ? null : co.getObject(type);
85
+ }
86
+
87
+ public <T> T remove(String key, Class<T> type) {
88
+ CachedObject co = data.remove(key);
89
+ return co == null ? null : co.getObject(type);
90
+ }
91
+
92
+ public Object remove(String key) {
93
+ CachedObject co = data.remove(key);
94
+ return co == null ? null : co.getObject();
95
+ }
96
+
97
+ public void clear() {
98
+ data.clear();
99
+ }
100
+
101
+ private class CachedObject {
102
+ Date expireAt;
103
+ Object object;
104
+
105
+ public CachedObject(Date date, Object obj) {
106
+ expireAt = date;
107
+ object = obj;
108
+ }
109
+
110
+ public Date getExpireAt() {
111
+ return expireAt;
112
+ }
113
+
114
+ public Object getObject() {
115
+ return object;
116
+ }
117
+
118
+ @SuppressWarnings("unchecked")
119
+ public <T> T getObject(Class<T> type) {
120
+ return (T) object;
121
+ }
122
+
123
+ }
124
+
125
+}
securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
....@@ -78,10 +78,12 @@
7878 * @param token
7979 * @return
8080 */
81
- public boolean validateToken(String token) {
81
+ public boolean isTokenValid(String token) {
8282 try {
8383 String tokenDecoded = new String(Base64.decode(token));
8484 String[] parts = StringUtils.split(tokenDecoded, ' ');
85
+ if (parts == null || parts.length < 3)
86
+ return false;
8587 String secret = parts[0];
8688 String user = parts[1];
8789 Date date = Utils.toDateFromIso(parts[2]);
....@@ -101,8 +103,24 @@
101103 try {
102104 String tokenDecoded = new String(Base64.decode(token));
103105 String[] parts = StringUtils.split(tokenDecoded, ' ');
106
+ if (parts == null || parts.length < 3)
107
+ return null;
104108 String user = parts[1];
105109 return user;
110
+ } catch (IOException e) {
111
+ log.error("Error decoding Base64 token", e);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ public Date extractDateCreationFromToken(String token) {
117
+ try {
118
+ String tokenDecoded = new String(Base64.decode(token));
119
+ String[] parts = StringUtils.split(tokenDecoded, ' ');
120
+ if (parts == null || parts.length < 3)
121
+ return null;
122
+ Date date = Utils.toDateFromIso(parts[2]);
123
+ return date;
106124 } catch (IOException e) {
107125 log.error("Error decoding Base64 token", e);
108126 }
....@@ -114,6 +132,6 @@
114132 String token = th.generateToken("pepe");
115133 System.out.println("Token: " + token);
116134 System.out.println("Token: " + new String(Base64.decode(token)));
117
- System.out.println("Valid Token: " + th.validateToken(token));
135
+ System.out.println("Valid Token: " + th.isTokenValid(token));
118136 }
119137 }
securis/src/main/resources/static/js/main.js
....@@ -67,11 +67,20 @@
6767 m.controller('MainCtrl', ['$scope', '$http', '$location', '$L', '$store',
6868 function($scope, $http, $location, $L, $store) {
6969
70
+ $location.path('/login');
7071 if ($store.get('token') != null) {
71
- $http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token');
72
- $location.path('/licenses');
73
- } else {
74
- $location.path('/login');
72
+
73
+ $http.get('/check', {
74
+ headers: {
75
+ 'X-SECURIS-TOKEN': $store.get('token')
76
+ }
77
+ }).success(function(data) {
78
+ if (data.valid) {
79
+ $http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token');
80
+ $location.path('/licenses');
81
+ $store.set('user', data.user);
82
+ }
83
+ });
7584 }
7685
7786 $scope.logout = function() {