From 83a0f7275c88183bb592b66ef3f2056cd7dea7f4 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Mon, 2 Mar 2015 21:20:25 -0600 Subject: [PATCH] Implemented token-based API authentication. Replaced the ApiKey concept with ephemeral tokens. Users and apps obtain a token by authenticating the user credentials (to be implemented). The service then generates a temporary token that is stored by the client and sent with every request using the `Authorization-Token` header. The server verifies this token to recognize and authenticate the request. With an authenticated user, the server can use the user's role to authorize requests. This implementation uses JSR 250 SecurityContext and security annotations. --- .../com/jdbernard/nlsongs/model/ApiKey.groovy | 7 --- .../com/jdbernard/nlsongs/model/Role.java | 3 ++ .../com/jdbernard/nlsongs/model/Token.groovy | 17 ++++++++ .../com/jdbernard/nlsongs/model/User.groovy | 9 ++++ .../security/NLSongsSecurityContext.groovy | 43 +++++++++++++++++++ .../rest/security/SecurityRequestFilter.java | 17 ++++++++ .../nlsongs/rest/security/TokenPrincipal.java | 35 +++++++++++++++ src/main/sql/create-db.sql | 21 +++++---- src/main/webapp/WEB-INF/web.xml | 5 +++ 9 files changed, 141 insertions(+), 16 deletions(-) delete mode 100644 src/main/groovy/com/jdbernard/nlsongs/model/ApiKey.groovy create mode 100644 src/main/groovy/com/jdbernard/nlsongs/model/Role.java create mode 100644 src/main/groovy/com/jdbernard/nlsongs/model/Token.groovy create mode 100644 src/main/groovy/com/jdbernard/nlsongs/rest/security/NLSongsSecurityContext.groovy create mode 100644 src/main/groovy/com/jdbernard/nlsongs/rest/security/SecurityRequestFilter.java create mode 100644 src/main/groovy/com/jdbernard/nlsongs/rest/security/TokenPrincipal.java diff --git a/src/main/groovy/com/jdbernard/nlsongs/model/ApiKey.groovy b/src/main/groovy/com/jdbernard/nlsongs/model/ApiKey.groovy deleted file mode 100644 index 06d1f02..0000000 --- a/src/main/groovy/com/jdbernard/nlsongs/model/ApiKey.groovy +++ /dev/null @@ -1,7 +0,0 @@ -package com.jdbernard.nlsongs.model - -public class ApiKey implements Serializable { - - String key - String description -} diff --git a/src/main/groovy/com/jdbernard/nlsongs/model/Role.java b/src/main/groovy/com/jdbernard/nlsongs/model/Role.java new file mode 100644 index 0000000..19451a8 --- /dev/null +++ b/src/main/groovy/com/jdbernard/nlsongs/model/Role.java @@ -0,0 +1,3 @@ +package com.jdbernard.nlsongs.model; + +public enum Role { admin, user } diff --git a/src/main/groovy/com/jdbernard/nlsongs/model/Token.groovy b/src/main/groovy/com/jdbernard/nlsongs/model/Token.groovy new file mode 100644 index 0000000..02fb335 --- /dev/null +++ b/src/main/groovy/com/jdbernard/nlsongs/model/Token.groovy @@ -0,0 +1,17 @@ +package com.jdbernard.nlsongs.model + +public class Token implements Serializable { + + String token + User user + Date expires + + @Override + public boolean equals(Object thatObj) { + if (thatObj == null) return false + if (!(thatObj instanceof Token)) return false + + Token that = (Token) thatObj + + return (this.token == that?.token) } +} diff --git a/src/main/groovy/com/jdbernard/nlsongs/model/User.groovy b/src/main/groovy/com/jdbernard/nlsongs/model/User.groovy index 18b17f5..410bd96 100644 --- a/src/main/groovy/com/jdbernard/nlsongs/model/User.groovy +++ b/src/main/groovy/com/jdbernard/nlsongs/model/User.groovy @@ -1,8 +1,17 @@ package com.jdbernard.nlsongs.model +import com.lambdaworks.crypto.SCryptUtil + public class User { int id String username String pwd + Role role + + public void setPwd(String pwd) { + this.pwd = SCryptUtil.scrypt(pwd, 16384, 16, 1) } + + public boolean checkPwd(String givenPwd) { + return SCryptUtil.check(this.pwd, givenPwd) } } diff --git a/src/main/groovy/com/jdbernard/nlsongs/rest/security/NLSongsSecurityContext.groovy b/src/main/groovy/com/jdbernard/nlsongs/rest/security/NLSongsSecurityContext.groovy new file mode 100644 index 0000000..1f5ff9e --- /dev/null +++ b/src/main/groovy/com/jdbernard/nlsongs/rest/security/NLSongsSecurityContext.groovy @@ -0,0 +1,43 @@ +package com.jdbernard.nlsongs.rest.security + +import java.security.Principal +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.core.SecurityContext + +import com.jdbernard.nlsongs.model.Role +import com.jdbernard.nlsongs.model.Token +import com.jdbernard.nlsongs.servlet.NLSongsContext + +public class NLSongsSecurityContext implements SecurityContext { + + public final TokenPrincipal principal + + public NLSongsSecurityContext(ContainerRequestContext ctx) { + + // Extract the authentication token (if present) + String tokenVal = ctx.getHeaderString("Authorization-Token") + + // Look up the token in the database. + Token token = NLSongsContext.songsDB.findToken(tokenVal) + + // Create our principal based on this token. + this.principal = new TokenPrincipal(token) + } + + @Override + public String getAuthenticationScheme() { return "Authentication-Token" } + + @Override + public Principal getUserPrincipal() { return principal } + + @Override + public boolean isSecure() { /* TODO */ return false } + + @Override + public boolean isUserInRole(String role) { + println "Required Role: $role" + println "User's Role: ${principal?.token?.user?.role}" + println "Required Role == User's Role? ${principal?.token?.user?.role == ((Role)role)} " + return principal?.token?.user?.role == ((Role) role) } +} + diff --git a/src/main/groovy/com/jdbernard/nlsongs/rest/security/SecurityRequestFilter.java b/src/main/groovy/com/jdbernard/nlsongs/rest/security/SecurityRequestFilter.java new file mode 100644 index 0000000..eef1b9a --- /dev/null +++ b/src/main/groovy/com/jdbernard/nlsongs/rest/security/SecurityRequestFilter.java @@ -0,0 +1,17 @@ +package com.jdbernard.nlsongs.rest.security; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.ext.Provider; + +import com.jdbernard.nlsongs.servlet.NLSongsContext; + +@Provider +@PreMatching +public class SecurityRequestFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext reqCtx) { + reqCtx.setSecurityContext(new NLSongsSecurityContext(reqCtx)); } +} diff --git a/src/main/groovy/com/jdbernard/nlsongs/rest/security/TokenPrincipal.java b/src/main/groovy/com/jdbernard/nlsongs/rest/security/TokenPrincipal.java new file mode 100644 index 0000000..6809f10 --- /dev/null +++ b/src/main/groovy/com/jdbernard/nlsongs/rest/security/TokenPrincipal.java @@ -0,0 +1,35 @@ +package com.jdbernard.nlsongs.rest.security; + +import java.security.Principal; + +import com.jdbernard.nlsongs.model.Token; + +public class TokenPrincipal implements Principal { + public final Token token; + + public TokenPrincipal(Token token) { this.token = token; } + + @Override + public boolean equals(Object thatObj) { + if (thatObj == null) return false; + if (!(thatObj instanceof TokenPrincipal)) return false; + + TokenPrincipal that = (TokenPrincipal) thatObj; + + if (this.token == null) { return that.token == null; } + else { return this.token.equals(that.token); } } + + @Override + public String getName() { + if (this.token == null) return null; + else return this.token.getUser().getUsername(); } + + @Override + public int hashCode() { + if (this.token == null) return 0; + return this.token.getUser().getUsername().hashCode(); } + + @Override + public String toString() { return getName(); } +} + diff --git a/src/main/sql/create-db.sql b/src/main/sql/create-db.sql index c892650..3455fa2 100644 --- a/src/main/sql/create-db.sql +++ b/src/main/sql/create-db.sql @@ -46,15 +46,18 @@ CREATE TABLE IF NOT EXISTS performances ( FOREIGN KEY (song_id) REFERENCES songs (id)); -DROP TABLE IF EXISTS api_keys; -CREATE TABLE IF NOT EXISTS api_keys ( - key VARCHAR(32) NOT NULL, - description VARCHAR(256) NOT NULL, - PRIMARY KEY (key)); - - DROP TABLE IF EXISTS users; CREATE TABLE IF NOT EXISTS users ( id SERIAL, - username VARCHAR(64), - pwd VARCHAR(80)); + username VARCHAR(64) UNIQUE NOT NULL, + pwd VARCHAR(80), + role VARCHAR(16) NOT NULL, + PRIMARY KEY (id)); + +DROP TABLE IF EXISTS tokens; +CREATE TABLE IF NOT EXISTS tokens ( + token VARCHAR(64), + user_id INTEGER NOT NULL, + expires TIMESTAMP NOT NULL, + PRIMARY KEY (token), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE); diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index b5081e7..8a74708 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -21,6 +21,11 @@ com.jdbernard.nlsongs.rest + + jersey.config.server.provider.classnames + org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature + + 1