You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by mc...@apache.org on 2015/12/01 17:43:48 UTC

[10/51] [abbrv] nifi git commit: NIFI-655. - Changed issuer field to use FQ class name because some classes return an empty string for getSimpleName(). - Finished refactoring JWT logic from request parsing logic in JwtService. - Updated AccessResource an

NIFI-655. - Changed issuer field to use FQ class name because some classes return an empty string for getSimpleName(). - Finished refactoring JWT logic from request parsing logic in JwtService. - Updated AccessResource and JwtAuthenticationFilter to call new JwtService methods decoupled from request header parsing. - Added extensive unit tests for JWT logic.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/7d04dfea
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/7d04dfea
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/7d04dfea

Branch: refs/heads/master
Commit: 7d04dfeac012307201e84dd00883dd6a79e6e2cc
Parents: 3bc11e1
Author: Andy LoPresto <an...@andylopresto.com>
Authored: Wed Nov 18 00:45:38 2015 -0800
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 08:31:39 2015 -0500

----------------------------------------------------------------------
 .../admin/service/impl/StandardKeyService.java  |  10 +-
 .../org/apache/nifi/web/api/AccessResource.java |  54 +++---
 .../security/jwt/JwtAuthenticationFilter.java   |  44 ++++-
 .../nifi/web/security/jwt/JwtService.java       |  64 +++----
 .../nifi/web/security/jwt/JwtServiceTest.java   | 182 ++++++++++++++++---
 5 files changed, 264 insertions(+), 90 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
index 1b2f8c9..7dff9d8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
@@ -18,6 +18,9 @@ package org.apache.nifi.admin.service.impl;
 
 import org.apache.nifi.admin.dao.DataAccessException;
 import org.apache.nifi.admin.service.AdministrationException;
+import org.apache.nifi.admin.service.KeyService;
+import org.apache.nifi.admin.service.action.GetKeyAction;
+import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 import org.apache.nifi.admin.service.transaction.Transaction;
 import org.apache.nifi.admin.service.transaction.TransactionBuilder;
 import org.apache.nifi.admin.service.transaction.TransactionException;
@@ -26,9 +29,6 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
-import org.apache.nifi.admin.service.KeyService;
-import org.apache.nifi.admin.service.action.GetKeyAction;
-import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 
 /**
  *
@@ -45,6 +45,8 @@ public class StandardKeyService implements KeyService {
 
     @Override
     public String getKey(String identity) {
+        // TODO: Change this service to look up by "key ID" instead of identity
+        // TODO: Change the return type to a Key POJO to support key rotation
         Transaction transaction = null;
         String key = null;
 
@@ -75,6 +77,8 @@ public class StandardKeyService implements KeyService {
 
     @Override
     public String getOrCreateKey(String identity) {
+        // TODO: Change this service to look up by "key ID" instead of identity
+        // TODO: Change the return type to a Key POJO to support key rotation
         Transaction transaction = null;
         String key = null;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index b4778ad..2e1c44e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -23,6 +23,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import io.jsonwebtoken.JwtException;
 import org.apache.nifi.util.NiFiProperties;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -184,30 +185,33 @@ public class AccessResource extends ApplicationResource {
                     accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
                     accessStatus.setMessage("No credentials supplied, unknown user.");
                 } else {
-                    // TODO - use this token with the JWT service
+                    // Extract the Base64 encoded token from the Authorization header
                     final String token = StringUtils.substringAfterLast(authorization, " ");
 
-                    // TODO - do not call this method of the jwt service
-                    final String principal = jwtService.getAuthentication(httpServletRequest);
-
-                    // TODO - catch jwt exception?
-                    // ensure we have something we can work with (certificate or credentials)
-                    if (principal == null) {
-                        throw new IllegalArgumentException("The specific token is not valid.");
-                    } else {
-                        // set the user identity
-                        accessStatus.setIdentity(principal);
-                        accessStatus.setUsername(CertificateUtils.extractUsername(principal));
-
-                        // without a certificate, this is not a proxied request
-                        final List<String> chain = Arrays.asList(principal);
-
-                        // check authorization for this user
-                        checkAuthorization(chain);
-
-                        // no issues with authorization
-                        accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                        accessStatus.setMessage("Account is active and authorized");
+                    try {
+                        final String principal = jwtService.getAuthenticationFromToken(token);
+
+                        // ensure we have something we can work with (certificate or credentials)
+                        if (principal == null) {
+                            throw new IllegalArgumentException("The specific token is not valid.");
+                        } else {
+                            // set the user identity
+                            accessStatus.setIdentity(principal);
+                            accessStatus.setUsername(CertificateUtils.extractUsername(principal));
+
+                            // without a certificate, this is not a proxied request
+                            final List<String> chain = Arrays.asList(principal);
+
+                            // check authorization for this user
+                            checkAuthorization(chain);
+
+                            // no issues with authorization
+                            accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
+                            accessStatus.setMessage("Account is active and authorized");
+                        }
+                    } catch (JwtException e) {
+                        // TODO: Handle the exception from a failed JWT verification
+                        throw new AccessDeniedException("The JWT could not be verified", e);
                     }
                 }
             } else {
@@ -334,7 +338,8 @@ public class AccessResource extends ApplicationResource {
                 }
                 
                 // create the authentication token
-                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getSimpleName());
+                // TODO: Some Spring beans return "" for getClass().getSimpleName(). Using getName() temporarily
+                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getName());
             } catch (final InvalidLoginCredentialsException ilce) {
                 throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
             } catch (final IdentityAccessException iae) {
@@ -355,7 +360,8 @@ public class AccessResource extends ApplicationResource {
             authorizeProxyIfNecessary(proxyChain);
 
             // create the authentication token
-            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), certificateIdentityProvider.getClass().getSimpleName());
+            // TODO: Some Spring beans return "" for getClass().getSimpleName(). Using getName() temporarily
+            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), certificateIdentityProvider.getClass().getName());
         }
 
         // generate JWT for response

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
index 5a84e93..dea5bba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
@@ -16,9 +16,8 @@
  */
 package org.apache.nifi.web.security.jwt;
 
-import java.util.Arrays;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import io.jsonwebtoken.JwtException;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
 import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
@@ -26,12 +25,18 @@ import org.apache.nifi.web.security.user.NewAccountRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+
 /**
  */
 public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
 
     private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
 
+    private static final String AUTHORIZATION = "Authorization";
+
     private JwtService jwtService;
 
     @Override
@@ -41,16 +46,35 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
             return null;
         }
 
+        // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource
+
         // get the principal out of the user token
-        final String jwtPrincipal = jwtService.getAuthentication(request);
-        if (jwtPrincipal == null) {
-            return null;
-        }
+        // look for an authorization token
+        final String authorization = request.getHeader(AUTHORIZATION);
 
-        if (isNewAccountRequest(request)) {
-            return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
+        // if there is no authorization header, we don't know the user
+        if (authorization == null) {
+            return null;
         } else {
-            return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
+            // Extract the Base64 encoded token from the Authorization header
+            final String token = StringUtils.substringAfterLast(authorization, " ");
+
+            try {
+                final String jwtPrincipal = jwtService.getAuthenticationFromToken(token);
+                if (jwtPrincipal == null) {
+                    return null;
+                }
+
+                if (isNewAccountRequest(request)) {
+                    return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
+                } else {
+                    return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
+                }
+            } catch (JwtException e) {
+                // TODO: Is this the correct way to handle an unverified token?
+                logger.error("Could not verify JWT", e);
+                return null;
+            }
         }
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
index acbbcfe..f006e5b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
@@ -33,7 +33,6 @@ import org.apache.nifi.admin.service.KeyService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.slf4j.LoggerFactory;
 
-import javax.servlet.http.HttpServletRequest;
 import java.nio.charset.StandardCharsets;
 import java.util.Calendar;
 
@@ -44,7 +43,8 @@ public class JwtService {
     private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
 
     private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
-    private final static String AUTHORIZATION = "Authorization";
+    private static final String KEY_ID_CLAIM = "kid";
+    private static final String USERNAME_CLAIM = "preferred_username";
 
     private final KeyService keyService;
 
@@ -52,49 +52,50 @@ public class JwtService {
         this.keyService = keyService;
     }
 
-    /**
-     * Gets the Authentication by extracting a JWT token from the specified request.
-     *
-     * @param request Request to extract the token from
-     * @return The user identifier from the token
-     */
-    public String getAuthentication(final HttpServletRequest request) {
-        // TODO: Refactor request token extraction out of this service
-        // extract/verify token from incoming request
-        final String authorization = request.getHeader(AUTHORIZATION);
-        final String base64EncodedToken = StringUtils.substringAfterLast(authorization, " ");
-
-        return getAuthenticationFromToken(base64EncodedToken);
-    }
-
-    public String getAuthenticationFromToken(final String base64EncodedToken) {
+    public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException {
         // The library representations of the JWT should be kept internal to this service.
         try {
             final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
+
+            if (jws == null) {
+                throw new JwtException("Unable to parse token");
+            }
+
+            // Additional validation that subject is present
+            if (StringUtils.isEmpty(jws.getBody().getSubject())) {
+                throw new JwtException("No subject available in token");
+            }
+
+            // TODO: Validate issuer against active registry?
+            if (StringUtils.isEmpty(jws.getBody().getIssuer())) {
+                // TODO: Remove after testing
+//                logger.info("Decoded JWT payload: " + jws.toString());
+                throw new JwtException("No issuer available in token");
+            }
             return jws.getBody().getSubject();
         } catch (JwtException e) {
             logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
-            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
+            final String errorMessage = "There was an error validating the JWT";
             logger.error(errorMessage, e);
-            throw new JwtException(errorMessage, e);
+            throw e;
         }
     }
 
     private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
         try {
-            // TODO: Check algorithm for validity
-            // TODO: Ensure signature verification occurs
             return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
                 @Override
                 public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                     final String identity = claims.getSubject();
 
+                    // TODO: Currently the kid field is identical to identity, but will be a unique key ID when key rotation is implemented
+                    final String keyId = claims.get(KEY_ID_CLAIM, String.class);
                     // The key is unique per identity and should be retrieved from the key service
-                    final String key = keyService.getKey(identity);
+                    final String key = keyService.getKey(keyId);
 
                     // Ensure we were able to find a key that was previously issued by this key service for this user
                     if (key == null) {
-                        throw new UnsupportedJwtException("Unable to determine signing key for " + identity);
+                        throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
                     }
 
                     return key.getBytes(StandardCharsets.UTF_8);
@@ -102,8 +103,7 @@ public class JwtService {
             }).parseClaimsJws(base64EncodedToken);
         } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
             // TODO: Exercise all exceptions to ensure none leak key material to logs
-            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
-            logger.error(errorMessage, e);
+            final String errorMessage = "There was an error validating the JWT";
             throw new JwtException(errorMessage, e);
         }
     }
@@ -111,9 +111,9 @@ public class JwtService {
     /**
      * Generates a signed JWT token from the provided (Spring Security) login authentication token.
      *
-     * @param authenticationToken
+     * @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source
      * @return a signed JWT containing the user identity and the identity provider, Base64-encoded
-     * @throws JwtException
+     * @throws JwtException if there is a problem generating the signed token
      */
     public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException {
         if (authenticationToken == null) {
@@ -144,17 +144,19 @@ public class JwtService {
 
             // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
 
+            // TODO: Change kid field to key ID when KeyService is refactored
+
             // Build the token
             return Jwts.builder().setSubject(identity)
                     .setIssuer(authenticationToken.getIssuer())
                     .setAudience(authenticationToken.getIssuer())
-                    .claim("preferred_username", username)
+                    .claim(USERNAME_CLAIM, username)
+                    .claim(KEY_ID_CLAIM, identity)
                     .setExpiration(expiration.getTime())
                     .setIssuedAt(Calendar.getInstance().getTime())
                     .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
         } catch (NullPointerException | AdministrationException e) {
-            // TODO: Remove exception handling and pass through
-            final String errorMessage = "Could not retrieve the signing key for JWT";
+            final String errorMessage = "Could not retrieve the signing key for JWT for " + identity;
             logger.error(errorMessage, e);
             throw new JwtException(errorMessage, e);
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
index a7e763e..c9107b9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
@@ -9,6 +9,7 @@ import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.codehaus.jettison.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
@@ -38,14 +39,38 @@ public class JwtServiceTest {
      * These constant strings were generated using the tool at http://jwt.io
      */
 
-    private static final String VALID_SIGNED_TOKEN = "";
-    private static final String INVALID_SIGNED_TOKEN = "";
-    private static final String VALID_UNSIGNED_TOKEN = "";
-    private static final String INVALID_UNSIGNED_TOKEN = "";
-    private static final String VALID_MALSIGNED_TOKEN = "";
-    private static final String INVALID_MALSIGNED_TOKEN = "";
+    private static final String VALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.8fUgH9jLvE1essgrcoV8OCyDhXvSXUH_1xqeqDqWycU";
+
+    // This token has an empty subject field
+    private static final String INVALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.jcjRBLtDzREmdjkJf3xry-ucyCmSRygBaP-HCWBkwlI";
+
+    private static final String VALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
+
+    // This token has an empty subject field
+    private static final String INVALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
+
+    // Algorithm field is "none"
+    private static final String VALID_MALSIGNED_TOKEN = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.mPO_wMNMl_zjMNevhNvUoXbSJ9Kx6jAe5OxDIAzKQbI";
+
+    // Algorithm field is "none" and no signature is present
+    private static final String VALID_MALSIGNED_NO_SIG_TOKEN = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.";
+
+    // This token has an empty subject field
+    private static final String INVALID_MALSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.WAwmUY4KHKV2oARNodkqDkbZsfRXGZfD2Ccy64GX9QF";
+
+    private static final String EXPIRED_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.y3M1TlzXZ80cVTkfcNxaHpq6aAlM1y2HGCZWEOcvmSU";
+
+    // Subject is "mgilman" but signed with "alopresto" key
+    private static final String IMPOSTER_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtZ2lsbWFuIiwiaXNzIjoiTW9ja0lkZW50aXR5UHJvdmlkZXIiLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI6ImFsb3ByZXN0byIsImV4cCI6MjQ0NzgwODc2MSwiaWF0IjoxNDQ3ODA4NzAxfQ.l-9nHmYTEMgLshX8qCEqbc2O4BH_GYBVQIFkUKsJvLA";
+
+    // Issuer field is set to unknown provider
+    private static final String UNKNOWN_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJVbmtub3duSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.SAd9tyNwSaijWet9wvAWSNmpxmPSK4XQuLx7h3ARqBo";
+
+    // Issuer field is absent
+    private static final String NO_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI6ImFsb3ByZXN0byIsImV4cCI6MjQ0NzgwODc2MSwiaWF0IjoxNDQ3ODA4NzAxfQ.Hdha7K69sz6224vidvuZ6A6UdGLdZ_U1egS0txuVXAk";
 
     private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}";
+    private static final String DEFAULT_IDENTITY = "alopresto";
 
     private static final String TOKEN_DELIMITER = ".";
 
@@ -93,6 +118,7 @@ public class JwtServiceTest {
     @Before
     public void setUp() throws Exception {
         mockKeyService = Mockito.mock(KeyService.class);
+        when(mockKeyService.getKey(anyString())).thenReturn(HMAC_SECRET);
         when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET);
         jwtService = new JwtService(mockKeyService);
     }
@@ -104,45 +130,156 @@ public class JwtServiceTest {
 
     @Test
     public void testShouldGetAuthenticationForValidToken() throws Exception {
+        // Arrange
+        String token = VALID_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
+        // Assert
+        assertEquals("Identity", DEFAULT_IDENTITY, identity);
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForInvalidToken() throws Exception {
         // Arrange
         String token = INVALID_SIGNED_TOKEN;
 
-        String header = "{" +
-                "  \"alg\":\"HS256\"" +
-                "}";
-        String payload = "{" +
-                "  \"sub\":\"alopresto\"," +
-                "  \"preferred_username\":\"alopresto\"," +
-                "  \"exp\":2895419760" +
-                "}";
-
         // Act
-        logger.info("Test token: " + generateHS256Token(header, payload, true, true));
-
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
         // Assert
 
-
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForEmptyToken() throws Exception {
+        // Arrange
+        String token = "";
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
+        // Assert
+
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForUnsignedToken() throws Exception {
+        // Arrange
+        String token = VALID_UNSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
 
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForMalsignedToken() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithmAndNoSignature() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_NO_SIG_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Ignore("Not yet implemented")
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenFromUnknownIdentityProvider() throws Exception {
+        // Arrange
+        String token = UNKNOWN_ISSUER_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenFromEmptyIdentityProvider() throws Exception {
+        // Arrange
+        String token = NO_ISSUER_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForExpiredToken() throws Exception {
+        // Arrange
+        String token = EXPIRED_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForImposterToken() throws Exception {
+        // Arrange
+        String token = IMPOSTER_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
 
+        // Should fail
     }
 
     @Test
@@ -176,6 +313,7 @@ public class JwtServiceTest {
         claims.put("iss", "MockIdentityProvider");
         claims.put("aud", "MockIdentityProvider");
         claims.put("preferred_username", "alopresto");
+        claims.put("kid", "alopresto");
         claims.put("exp", TOKEN_EXPIRATION_SEC);
         claims.put("iat", ISSUED_AT_SEC);
         logger.trace("JSON Object to String: " + new JSONObject(claims).toString());