getUserAddOns();
@@ -109,11 +130,16 @@ public Boolean userAffectedByPricing() {
/**
* Returns a list with the full subscription contracted by the current user
* (including plans and add-ons).
+ *
+ * There are two keys inside this {@link Map}:
+ *
+ * - Key {@code plans} contains the plan name of the user
+ *
- Key {@code addOns} contains a list with the add-ons contracted by the
+ * user
+ *
*
- * Key "plan" contains the plan name of the user.
- * Key "addOns" contains a list with the add-ons contracted by the user.
- *
- * @return {@code Map} with the current user's contracted subscription.
+ * @return {@code Map} with the current user's contracted
+ * subscription.
*/
public final Map getUserSubscription() {
Map userSubscription = new HashMap<>();
@@ -164,7 +190,7 @@ public final Map getPlanContext() {
* This method returns the {@link PricingManager} object that is being used to
* evaluate the pricing plan.
*
- * @return PricingManager object
+ * @return {@link PricingManager} object
*/
public final PricingManager getPricingManager() {
try {
diff --git a/src/main/java/io/github/isagroup/PricingEvaluatorUtil.java b/src/main/java/io/github/isagroup/PricingEvaluatorUtil.java
index 29d9d92..29e1101 100644
--- a/src/main/java/io/github/isagroup/PricingEvaluatorUtil.java
+++ b/src/main/java/io/github/isagroup/PricingEvaluatorUtil.java
@@ -1,17 +1,20 @@
package io.github.isagroup;
-import java.util.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
import java.util.logging.Logger;
-import io.github.isagroup.models.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.stereotype.Component;
import io.github.isagroup.exceptions.PricingPlanEvaluationException;
+import io.github.isagroup.models.Feature;
+import io.github.isagroup.models.FeatureStatus;
+import io.github.isagroup.models.PlanContextManager;
+import io.github.isagroup.models.PricingManager;
import io.github.isagroup.services.jwt.PricingJwtUtils;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
/**
* Utility class that provides methods to generate and manage JWT that contains
@@ -58,23 +61,19 @@ public String generateUserToken() {
planContextManager.setUserContext(pricingContext.getUserContext());
claims.put("userContext", planContextManager.getUserContext());
} catch (Exception e) {
- throw new PricingPlanEvaluationException("Error while retrieving user context! Please check your PricingContext.getUserContext() method");
+ throw new PricingPlanEvaluationException(
+ "Error while retrieving user context! Please check your PricingContext.getUserContext() method");
}
if (!pricingContext.userAffectedByPricing()) {
- return Jwts.builder()
- .setClaims(claims)
- .setSubject(subject)
- .setIssuedAt(new Date(System.currentTimeMillis()))
- .setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
- .signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
- .compact();
+ return jwtUtils.createJwtToken(claims, subject);
}
try {
planContextManager.setPlanContext(pricingContext.getPlanContext());
} catch (NullPointerException e) {
- throw new PricingPlanEvaluationException("Error while retrieving plan context! Please check your configuration file or add a plan with the given name");
+ throw new PricingPlanEvaluationException(
+ "Error while retrieving plan context! Please check your configuration file or add a plan with the given name");
}
PricingManager pricingManager = pricingContext.getPricingManager();
@@ -86,18 +85,11 @@ public String generateUserToken() {
claims.put("features", featureStatuses);
claims.put("planContext", planContextManager.getPlanContext());
- return Jwts.builder()
- .setClaims(claims)
- .setSubject(subject)
- .setIssuedAt(new Date(System.currentTimeMillis()))
- .setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
- .signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
- .compact();
+ return jwtUtils.createJwtToken(claims, subject);
}
-
private Map computeFeatureStatuses(PlanContextManager planContextManager,
- Map features) {
+ Map features) {
Map featureStatuses = new HashMap<>();
@@ -109,10 +101,11 @@ private Map computeFeatureStatuses(PlanContextManager pla
String expression = features.get(featureName).getExpression();
try {
Boolean eval = FeatureStatus.computeFeatureEvaluation(expression, planContextManager)
- .orElseThrow(() -> new PricingPlanEvaluationException("Evaluation was null"));
+ .orElseThrow(() -> new PricingPlanEvaluationException("Evaluation was null"));
featureStatus.setEval(eval);
} catch (SpelEvaluationException e) {
- throw new PricingPlanEvaluationException("Error while evaluating the expression of the feature " + featureName + "! Please check the expression");
+ throw new PricingPlanEvaluationException("Error while evaluating the expression of the feature "
+ + featureName + "! Please check the expression");
}
Optional userContextKey = FeatureStatus.computeUserContextVariable(expression);
@@ -124,9 +117,12 @@ private Map computeFeatureStatuses(PlanContextManager pla
featureStatus.setUsed(planContextManager.getUserContext().get(userContextKey.get()));
if (feature.getExpression().contains("usageLimits")) {
String usageLimitName = feature.getExpression().split("usageLimits")[1].split("[',\"]")[2];
- featureStatus.setLimit(((Map) planContextManager.getPlanContext().get("usageLimits")).get(usageLimitName));
+ featureStatus
+ .setLimit(((Map) planContextManager.getPlanContext().get("usageLimits"))
+ .get(usageLimitName));
} else {
- featureStatus.setLimit(((Map) planContextManager.getPlanContext().get("features")).get(featureName));
+ featureStatus.setLimit(((Map) planContextManager.getPlanContext().get("features"))
+ .get(featureName));
}
}
@@ -148,7 +144,7 @@ private Map computeFeatureStatuses(PlanContextManager pla
* @param expression the expression of the feature that will replace its
* evaluation
* @return Modified version of the provided JWT that contains the new expression
- * in the "eval" attribute of the feature.
+ * in the "eval" attribute of the feature.
*/
public String addExpressionToToken(String token, String featureId, String expression) {
@@ -173,13 +169,7 @@ private String buildJwtToken(Map> features, String s
claims.put("userContext", pricingContext.getUserContext());
claims.put("planContext", pricingContext.getPlanContext());
- return Jwts.builder()
- .setClaims(claims)
- .setSubject(subject)
- .setIssuedAt(new Date(System.currentTimeMillis()))
- .setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
- .signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
- .compact();
+ return jwtUtils.createJwtToken(claims, subject);
}
}
diff --git a/src/main/java/io/github/isagroup/services/jwt/PricingJwtUtils.java b/src/main/java/io/github/isagroup/services/jwt/PricingJwtUtils.java
index ede532f..2e2ddb4 100644
--- a/src/main/java/io/github/isagroup/services/jwt/PricingJwtUtils.java
+++ b/src/main/java/io/github/isagroup/services/jwt/PricingJwtUtils.java
@@ -1,9 +1,10 @@
package io.github.isagroup.services.jwt;
import java.util.Date;
-import java.util.HashMap;
import java.util.Map;
+import javax.crypto.SecretKey;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,12 +12,11 @@
import io.github.isagroup.PricingContext;
import io.github.isagroup.PricingEvaluatorUtil;
-import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.MalformedJwtException;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
/**
* Utility class that provides methods to generate and manage JWT.
@@ -24,117 +24,132 @@
@Component
public class PricingJwtUtils {
- @Autowired
- private PricingContext pricingContext;
-
- public PricingJwtUtils(PricingContext pricingContext) {
- this.pricingContext = pricingContext;
- }
-
- private static final Logger logger = LoggerFactory.getLogger(PricingJwtUtils.class);
-
- /**
- * Extracts the subject from the given JWT.
- *
- * @param token a JWT that contains a subject
- * @return The subject of the JWT
- */
- public String getSubjectFromJwtToken(String token) {
- return Jwts.parser().setSigningKey(pricingContext.getJwtSecret()).parseClaimsJws(token).getBody().getSubject();
- }
-
- /**
- * Generates a JWT from the given username that does not contains pricing
- * evaluation.
- *
- * @param username The username to be used as the subject of the JWT
- * @return The generated JWT
- */
- public String generateTokenFromUsername(String username) {
- Map claims = new HashMap<>();
- return Jwts.builder().setClaims(claims).setSubject(username).setIssuedAt(new Date())
- .setExpiration(new Date((new Date()).getTime() + pricingContext.getJwtExpiration()))
- .signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret()).compact();
- }
-
- /**
- * Extracts the features from a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- *
- * @param token a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- * @return A Map that contains the evaluation of all the features for the
- * current user located in the JWT body
- */
- public Map> getFeaturesFromJwtToken(String token) {
- return (Map>) Jwts.parser().setSigningKey(pricingContext.getJwtSecret())
- .parseClaimsJws(token).getBody().get("features");
- }
-
- /**
- * Extracts the plan context from a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- *
- * @param token a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- * @return A Map that contains the plan context used in the evaluation of
- * features located in the JWT body
- */
- public Map getPlanContextFromJwtToken(String token) {
- return (Map) Jwts.parser().setSigningKey(pricingContext.getJwtSecret()).parseClaimsJws(token)
- .getBody().get("planContext");
- }
-
- /**
- * Extracts the user context from a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- *
- * @param token a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- * @return A Map that contains the user context used in the evaluation of
- * features located in the JWT body
- */
- public Map getUserContextFromJwtToken(String token) {
- return (Map) Jwts.parser().setSigningKey(pricingContext.getJwtSecret()).parseClaimsJws(token)
- .getBody().get("userContext");
- }
-
- /**
- * Extracts the username from a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method.
- * It is the equivalent of getting the token's subject
- *
- * @param token a JWT generated by
- * {@link PricingEvaluatorUtil#generateUserToken()} method
- * @return The username of the user located in the JWT body
- */
- public String getUserNameFromJwtToken(String token) {
- return Jwts.parser().setSigningKey(pricingContext.getJwtSecret()).parseClaimsJws(token).getBody().getSubject();
- }
-
- /**
- * Validates a JWT
- * It is the equivalent of getting the token's subject
- *
- * @param authToken a JWT String
- * @return true if the token is correct, false otherwise
- */
- public boolean validateJwtToken(String authToken) {
- try {
- Jwts.parser().setSigningKey(pricingContext.getAuthJwtSecret()).parseClaimsJws(authToken);
- return true;
- } catch (SignatureException e) {
- logger.error("Invalid JWT signature: {}", e.getMessage());
- } catch (MalformedJwtException e) {
- logger.error("Invalid JWT: {}", e.getMessage());
- } catch (ExpiredJwtException e) {
- logger.error("JWT is expired: {}", e.getMessage());
- } catch (UnsupportedJwtException e) {
- logger.error("JWT is unsupported: {}", e.getMessage());
- } catch (IllegalArgumentException e) {
- logger.error("JWT claims string is empty: {}", e.getMessage());
- }
-
- return false;
- }
+ @Autowired
+ private PricingContext pricingContext;
+
+ private final SecretKey pricingSecretKey;
+
+ public PricingJwtUtils(PricingContext pricingContext) {
+ this.pricingContext = pricingContext;
+ this.pricingSecretKey = createKeyForBase64String(pricingContext.getJwtSecret());
+ }
+
+ /**
+ * Given a base64 encoded {@link String} creates a {@link SecretKey} to be used
+ * when signing a JWT token. Given string must meet HMAC-SHA bit length
+ * requirements
+ *
+ * @param base64String
+ * @return {@link javax.crypto.SecretKey}
+ * @throws {@link io.jsonwebtoken.security.WeakKeyException} if the string is
+ * not strong enough to be used with HMAC-SHA algorithms
+ */
+ private static SecretKey createKeyForBase64String(String base64String) {
+ return Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64String));
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PricingJwtUtils.class);
+
+ /**
+ * Given a map claims and subject creates a JWT token given
+ * {@link PricingContext} configuation
+ *
+ * @param claims a {@link Map} of claims
+ * @param subject a target of the token
+ * @return The subject of the JWT
+ */
+ public String createJwtToken(Map claims, String subject) {
+
+ return Jwts.builder()
+ .claims(claims)
+ .subject(subject)
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
+ .signWith(pricingSecretKey)
+ .compact();
+ }
+
+ /**
+ * Extracts the subject from the given JWT.
+ *
+ * @param token a JWT that contains a subject
+ * @return The subject of the JWT
+ */
+ public String getSubjectFromJwtToken(String token) {
+ return Jwts.parser().verifyWith(pricingSecretKey)
+ .build()
+ .parseSignedClaims(token).getPayload().getSubject();
+ }
+
+ /**
+ * Extracts the features from a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ *
+ * @param token a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ * @return A Map that contains the evaluation of all the features for the
+ * current user located in the JWT body
+ */
+ public Map> getFeaturesFromJwtToken(String token) {
+ return (Map>) Jwts.parser()
+ .verifyWith(pricingSecretKey).build()
+ .parseSignedClaims(token)
+ .getPayload().get("features");
+ }
+
+ /**
+ * Extracts the plan context from a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ *
+ * @param token a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ * @return A Map that contains the plan context used in the evaluation of
+ * features located in the JWT body
+ */
+ public Map getPlanContextFromJwtToken(String token) {
+ return (Map) Jwts.parser()
+ .verifyWith(pricingSecretKey).build()
+ .parseSignedClaims(token)
+ .getPayload().get("planContext");
+ }
+
+ /**
+ * Extracts the user context from a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ *
+ * @param token a JWT generated by
+ * {@link PricingEvaluatorUtil#generateUserToken()} method
+ * @return A Map that contains the user context used in the evaluation of
+ * features located in the JWT body
+ */
+ public Map getUserContextFromJwtToken(String token) {
+ return (Map) Jwts.parser()
+ .verifyWith(pricingSecretKey).build()
+ .parseSignedClaims(token)
+ .getPayload().get("userContext");
+ }
+
+ /**
+ * Validates a JWT
+ * It is the equivalent of getting the token's subject
+ *
+ * @param authToken a JWT String
+ * @return true if the token is correct, false otherwise
+ */
+ public boolean validateJwtToken(String authToken) {
+ try {
+ Jwts.parser().verifyWith(createKeyForBase64String(pricingContext.getAuthJwtSecret()))
+ .build()
+ .parseSignedClaims(authToken);
+ return true;
+ } catch (UnsupportedJwtException e) {
+ LOGGER.error("jwt {} argument does not represent a signed Claims JWT: ", authToken, e);
+ } catch (JwtException e) {
+ LOGGER.error("jwt {} cannot be parsed or validated as required: ", authToken, e);
+ } catch (IllegalArgumentException e) {
+ LOGGER.error("Illegal argument for token", e);
+ }
+
+ return false;
+ }
}
diff --git a/src/test/java/io/github/isagroup/PricingContextTestImpl.java b/src/test/java/io/github/isagroup/PricingContextTestImpl.java
index ac9e91f..8b5fa18 100644
--- a/src/test/java/io/github/isagroup/PricingContextTestImpl.java
+++ b/src/test/java/io/github/isagroup/PricingContextTestImpl.java
@@ -4,8 +4,13 @@
import java.util.List;
import java.util.Map;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.io.Encoders;
+
public class PricingContextTestImpl extends PricingContext {
+ static final String JWT_SUBJECT_TEST = "admin1";
+
private String path;
private String secret;
private Integer jwtExpiration;
@@ -14,12 +19,12 @@ public class PricingContextTestImpl extends PricingContext {
private Map userContext;
public PricingContextTestImpl() {
- this.path = null;
- this.secret = "defualtSecret";
+ this.path = "pricing/petclinic.yml";
+ this.secret = Encoders.BASE64.encode(Jwts.SIG.HS256.key().build().getEncoded());
this.jwtExpiration = 86400;
- this.userPlan = null;
+ this.userPlan = "ADVANCED";
this.userAddOns = new ArrayList<>();
- this.userContext = null;
+ this.userContext = Map.of("username", JWT_SUBJECT_TEST, "pets", 2);
}
@Override
diff --git a/src/test/java/io/github/isagroup/PricingEvaluatorUtilTests.java b/src/test/java/io/github/isagroup/PricingEvaluatorUtilTests.java
index c66427c..016440d 100644
--- a/src/test/java/io/github/isagroup/PricingEvaluatorUtilTests.java
+++ b/src/test/java/io/github/isagroup/PricingEvaluatorUtilTests.java
@@ -1,9 +1,7 @@
package io.github.isagroup;
-import java.util.HashMap;
import java.util.Map;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import io.github.isagroup.services.jwt.PricingJwtUtils;
@@ -12,39 +10,11 @@
public class PricingEvaluatorUtilTests {
- private static final String JWT_SECRET_TEST = "secret";
- private static final Integer JWT_EXPIRATION_TEST = 86400;
- private static final String JWT_SUBJECT_TEST = "admin1";
- private static final String JWT_EXPRESSION_TEST = "userContext['pets']*4 < planContext['usageLimits']['pets']";
+ private PricingContext pricingContext = new PricingContextTestImpl();
- private static final String USER_PLAN = "ADVANCED";
- private static final String YAML_CONFIG_PATH = "pricing/petclinic.yml";
+ private PricingEvaluatorUtil pricingEvaluatorUtil = new PricingEvaluatorUtil(pricingContext);
- private PricingContext pricingContext;
-
- private PricingEvaluatorUtil pricingEvaluatorUtil;
-
- private PricingJwtUtils jwtUtils;
-
- @BeforeEach
- public void setUp() {
-
- Map userContext = new HashMap<>();
- userContext.put("username", JWT_SUBJECT_TEST);
- userContext.put("pets", 2);
-
- PricingContextTestImpl pricingContext = new PricingContextTestImpl();
-
- pricingContext.setJwtExpiration(JWT_EXPIRATION_TEST);
- pricingContext.setJwtSecret(JWT_SECRET_TEST);
- pricingContext.setUserContext(userContext);
- pricingContext.setUserPlan(USER_PLAN);
- pricingContext.setConfigFilePath(YAML_CONFIG_PATH);
-
- this.pricingContext = pricingContext;
- this.pricingEvaluatorUtil = new PricingEvaluatorUtil(pricingContext);
- this.jwtUtils = new PricingJwtUtils(pricingContext);
- }
+ private PricingJwtUtils jwtUtils = new PricingJwtUtils(pricingContext);
@Test
void simpleTokenGenerationTest() {
@@ -71,7 +41,7 @@ void checkTokenSubjectTest() {
String jwtSubject = jwtUtils.getSubjectFromJwtToken(token);
assertTrue(jwtUtils.validateJwtToken(token), "Token is not valid");
- assertEquals(JWT_SUBJECT_TEST, jwtSubject, "The subject has not being correctly set");
+ assertEquals(PricingContextTestImpl.JWT_SUBJECT_TEST, jwtSubject, "The subject has not being correctly set");
}
@@ -79,14 +49,15 @@ void checkTokenSubjectTest() {
void tokenExpressionsTest() {
String firstToken = pricingEvaluatorUtil.generateUserToken();
+ String jwtExpressionTest = "userContext['pets']*4 < planContext['usageLimits']['pets']";
String newToken = pricingEvaluatorUtil.addExpressionToToken(firstToken, "visits",
- JWT_EXPRESSION_TEST);
+ jwtExpressionTest);
Map> features = jwtUtils.getFeaturesFromJwtToken(newToken);
assertTrue(jwtUtils.validateJwtToken(newToken), "Token is not valid");
- assertEquals(JWT_EXPRESSION_TEST, (String) features.get("visits").get("eval"),
+ assertEquals(jwtExpressionTest, (String) features.get("visits").get("eval"),
"The expression for the feature visits has not being correctly set");
}
diff --git a/src/test/java/io/github/isagroup/PricingPlanAwareTests.java b/src/test/java/io/github/isagroup/PricingPlanAwareTests.java
index ae561af..d8256c8 100644
--- a/src/test/java/io/github/isagroup/PricingPlanAwareTests.java
+++ b/src/test/java/io/github/isagroup/PricingPlanAwareTests.java
@@ -34,7 +34,7 @@
io.github.isagroup.services.jwt.PricingJwtUtils.class })
public class PricingPlanAwareTests {
- private static final String JWT_SECRET_TEST = "secret";
+ private static final String JWT_SECRET_TEST = "qfqj73ZGIN1XxPvI5mG6dVaXqpY4XVeOOBjp4zf0yNE=";
private static final Integer JWT_EXPIRATION_TEST = 86400;
private static final String JWT_SUBJECT_TEST = "admin1";
private static final String CONFIG_FILE_PATH_TEST = "pricing/petclinic.yml";
diff --git a/src/test/java/io/github/isagroup/pricingcontext/PricingConfigExpirationTest.java b/src/test/java/io/github/isagroup/pricingcontext/PricingConfigExpirationTest.java
index 1a5c60d..043384a 100644
--- a/src/test/java/io/github/isagroup/pricingcontext/PricingConfigExpirationTest.java
+++ b/src/test/java/io/github/isagroup/pricingcontext/PricingConfigExpirationTest.java
@@ -13,6 +13,8 @@
import io.github.isagroup.PricingContext;
import io.github.isagroup.PricingEvaluatorUtil;
import io.github.isagroup.services.jwt.PricingJwtUtils;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.io.Encoders;
public class PricingConfigExpirationTest {
@@ -38,7 +40,7 @@ public String getConfigFilePath() {
@Override
public String getJwtSecret() {
- return "p3tclinic";
+ return Encoders.BASE64.encode(Jwts.SIG.HS256.key().build().getEncoded());
}
@Override