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 1b2f8c9365..7dff9d811a 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; 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 b4778ad783..2e1c44e18b 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); + try { + final String principal = jwtService.getAuthenticationFromToken(token); - // 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)); + // 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 chain = Arrays.asList(principal); + // without a certificate, this is not a proxied request + final List chain = Arrays.asList(principal); - // check authorization for this user - checkAuthorization(chain); + // check authorization for this user + checkAuthorization(chain); - // no issues with authorization - accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); - accessStatus.setMessage("Account is active and authorized"); + // 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 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 5a84e93643..dea5bba295 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; } - // get the principal out of the user token - final String jwtPrincipal = jwtService.getAuthentication(request); - if (jwtPrincipal == null) { - return null; - } + // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource - if (isNewAccountRequest(request)) { - return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request))); + // get the principal out of the user token + // look for an authorization token + final String authorization = request.getHeader(AUTHORIZATION); + + // 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; + } } } 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 acbbcfe58c..f006e5be95 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 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 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); } 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 a7e763e78d..c9107b9ebc 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 - public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception { + @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());