by Horatiu Dan
Context
In my opinion, the purpose of all software applications that have been created so far, are being and will be developed should primarily be to make humans’ day to day activities easier to fulfill. Humans are the most valuable creations and software applications are great tools that at least could be used by them.
Nowadays, almost every software product exchanges data with at least one other peer software product, which results in huge amounts of data flowing among them. Usually, a request from one product to another needs to pass a set of preconditions before it is considered acceptable and trustworthy.
The purpose of this article is to showcase a simple and flexible, yet efficient and decoupled solution for validating such prerequisites.
Setting the Stage
Let’s consider the next simple and general use case:
- Service Provider and Client are two applications exchanging data.
- The Client calls the Service Provider.
- The operation invoked is executed only after the Client is identified by the Service Provider.
- The Client identification is done via a token included in the request and validated by the Service Provider.
As part of this article, a small Java project is built and while doing this the token validation strategy is explained.
As JSON Web Tokens (JWT) are widely used nowadays, especially when products need to identify among others, JWT validation was chosen as the concrete implementation. According to RFC7519, a JWT is a compact, encoded, URL-safe string representation of a JSON message.
Very briefly, a JWT has three sections – header, payload and signature.
Encoded, it is a string with three sections, separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJoY2QiLCJpc3MiOiJpc3N1ZXIiLCJhdWQiOiJhdWRpZW5jZSIsImV4cCI6MTY1MDU0OTg1OH0. rbs6NqNw9KZ4IGuCOjdPpdJqMswTXHn7oNADCzlQHL8
Decoded, it is in JSON format and thus, more readable:
Header – algorithm and type
{ "alg": "HS256", "typ": "JWT" }
Payload – data (claims)
{ "sub": "hcd", "iss": "issuer", "aud": "audience", "exp": 1650549858 }
Signature
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), the-256-bit-secret )
These pieces of information are enough to have an idea about JWTs, let’s start developing.
Initial Implementation
The sample project is built with Java 17 and Maven. The dependencies are very few:
- io.jsonwebtoken / jjwt – for JWT signing and verification
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
For exploring other available libraries, check https://jwt.io/libraries?language=Java.
- JUnit 5 and Mockito – for unit testing the implementation
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.5.1</version> <scope>test</scope> </dependency>
JWT generation and verification is implemented using the following interface:
public interface JwtManager { String generate(String sub, String iss, String aud); boolean isValid(String jwt, String iss, String aud); }
The former method uses the provided parameters (subject, issuer and audience) to create and sign a valid a JWT. The latter checks whether the jwt is valid or not, using the provided issuer and audience.
The goal is to create an implementation and make the following test pass.
class JwtManagerTest { private String iss; private String aud; private String jwt; private JwtManager jwtManager; @BeforeEach void setUp() { jwtManager = new JwtManagerImpl(); iss = "issuer"; aud = "audience"; jwt = jwtManager.generate("hcd", iss, aud); Assertions.assertNotNull(jwt); } @Test void isValid_coupled() { final boolean valid = jwtManager.isValid(jwt, iss, aud); Assertions.assertTrue(valid); } }
By leveraging the Jwts
builder, the sub,
iss, aud are set, the token is configured to
expire after 1 minute and moreover, it is signed using the Service
Provider secret key.
public String generate(String sub, String iss, String aud) { final Date exp = new Date(System.currentTimeMillis() + 60_000); return Jwts.builder() .setSubject(sub) .setIssuer(iss) .setAudience(aud) .setExpiration(exp) .signWith(SignatureAlgorithm.HS256, "s1e2c3r4e5t6k7e8y9") .compact(); }
In the other direction, the token is parsed using the same secret key and if it hasn’t expired yet, the payload claims are extracted.
public boolean isValid(String jwt, String iss, String aud) { Claims body; try { body = Jwts.parser() .setSigningKey("s1e2c3r4e5t6k7e8y9") .parseClaimsJws(jwt) .getBody(); } catch (JwtException e) { return false; } return iss.equals(body.getIssuer()) && aud.equals(body.getAudience()); }
This is straight-forward. Nevertheless, a custom assumption is made in addition to the standard (mandatory) token validations.
“A valid token is acceptable if the issuer and audience conform to specific values.”
Basically this is the plot of the article – how to implement the custom verification for a valid token, as flexible as possible.
If we run the test, it passes, the implementation is correct, but unfortunately, not flexible enough.
At some point, the Service Provider that validates the Client
request changes the assumption that has been previously made. This
obviously impacts isValid()
method whose
implementation should to be changed.
Final Implementation
It would be good if whenever the Service Provider makes a change to these preconditions, the standard part of the token validation remains in place. Then the code shall be flexible enough to allow deciding on the custom validation assumptions as late as possible. In order to accommodate this, the code needs to be refactored.
What’s been stated, it’s enclosed in the next interface (even
better, @FunctionalInterface
).
@FunctionalInterface public interface ValidationStrategy { boolean isValid(Claims body); }
The strategy is implemented, the last two lines in the
isValid()
method are moved in the newly implemented
strategy. Moreover, we may assume that this is the default
validation strategy of the Service Provider.
public class DefaultValidationStrategy implements ValidationStrategy { private final String iss; private final String aud; public DefaultValidationStrategy(String iss, String aud) { this.iss = iss; this.aud = aud; } @Override public boolean isValid(Claims body) { return iss.equals(body.getIssuer()) && aud.equals(body.getAudience()); } }
The former method is first deprecated and soon replaced by the new implementation below.
public interface JwtManager { String generate(String sub, String iss, String aud); /** * @deprecated in favor of {@link #isValid(String, ValidationStrategy)} */ @Deprecated(forRemoval = true) boolean isValid(String jwt, String iss, String aud); boolean isValid(String jwt, ValidationStrategy strategy); }
Basically, the new method delegates to the
ValidationStrategy
callback. Delegation (in
programming) means exactly this, one entity passes something to
another entity.
public boolean isValid(String jwt, ValidationStrategy strategy) { Claims body; try { body = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(jwt) .getBody(); } catch (JwtException e) { return false; } return strategy.isValid(body); }
In use, the validation is performed as in the following unit test.
@Test void isValid_looselyCoupled_defaultStrategy() { final boolean valid = jwtManager.isValid(jwt, new DefaultValidationStrategy(iss, aud)); Assertions.assertTrue(valid); }
With these modifications, the code is flexible enough to accommodate potential changes in the validation strategy. For instance, if the Service Provider decides to check only the issuer, this can be achieved without needing to modify the code that handles the JWT standard part.
@Test void isValid_looselyCoupled_customStrategy() { final boolean valid = jwtManager.isValid(jwt, body -> iss.equals(body.getIssuer())); Assertions.assertTrue(valid); }
If we have a look at the previous unit test, we see how handful
it is to pass the ValidationStrategy
using lambda.
Also, I suppose it’s clear the reason for making the
ValidationStrategy
a @FunctionalInterface from the
beginning.
Conclusion
In this article, a decoupled solution for validating JSON Web Tokens was implemented. This solution uses callbacks and thus promotes decoupling and flexibility.
Resources
- JWT.IO – https://jwt.io/
- RFC7519 – https://datatracker.ietf.org/doc/html/rfc7519
- Sample project is available here.
- Picture – https://jaysbrickblog.com/