Yet another JWT library (pac4j-jwt) for Java
pac4j is a security engine for Java which supports authentication/authorization and is available for many frameworks: JEE, Play, Vertx, Spring Security, Shiro and Ratpack to name only a few.
Regarding authentication, one very common use case is to have a user authenticating via a browser (Facebook login for example) and then turn his identity into something usable for calling web services. This could be an opaque string, but it requires checking the value to get the identity user. So the best solution would be some self-sufficient information.
It is here that JWT comes into play: a JSON Web Token is a JSON object which can be signed and/or encrypted and is encoded in base64 format. It’s an industry standard. Signature is used to ensure that the JWT has not been tampered. Encryption is used to hide the information held by the JWT.
A JWT consists of three parts: a header, a payload and a signature. Here is an example taken from the jwt.io website. The value:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9. TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
is decoded as:
Header | Payload | Signature |
---|---|---|
{ "alg": "HS256", "typ": "JWT" } |
{ "sub": "1234567890", "name": "John Doe", "admin": true } |
To be verified |
For Java, there are several good JWT libraries, but I think the best one is the Nimbus JOSE JWT: it may not be the easiest one, but it really has everything you need for JWT support. You should especially read their algorithm selection guide.
Let’s take an example with HMAC signature:
JWTClaimsSet claimsSet = new JWTClaimsSet();
claimsSet.setSubject("alice");
claimsSet.setIssuer("https://c2id.com");
JWSSigner signer = new MACSigner(KEY1);
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(signer);
String jwt = signedJWT.serialize();
signedJWT = SignedJWT.parse(jwt);
JWSVerifier verifier = new MACVerifier(sharedSecret);
assertTrue(signedJWT.verify(verifier));
assertEquals("alice", signedJWT.getJWTClaimsSet().getSubject());
assertEquals("https://c2id.com", signedJWT.getJWTClaimsSet().getIssuer());
It’s really not too complicated and all your use cases can be handled with a similar logic. Even if it provides low-level abstractions, it should be more than sufficient in most situations.
In pac4j security libraries, things are slightly different and authenticated users have profiles (with identifiers, attributes and roles).
So we must generate JWTs from these profiles (it’s done via the JwtGenerator
) and verify them during the authentication process (thanks to the JwtAuthenticator
).
We can also verify JWT created in other applications without using the JwtGenerator
to address other use cases.
Both the JwtGenerator
and JwtAuthenticator
are available in the pac4j-jwt
module (groupId: org.pac4j
).
As we want to be able to handle more than just one encryption/signature use case in the JwtAuthenticator
, we offer higher abstractions to configure signature and encryption: the SignatureConfiguration
and the EncryptionConfiguration
classes.
Inspired by the pac4j JWT documentation:
JwtGenerator<FacebookProfile> generator = new JwtGenerator<>(new SecretSignatureConfiguration(KEY1), new SecretEncryptionConfiguration(KEY1));
String token = generator.generate(facebookProfile);
JwtAuthenticator jwtAuthenticator = new JwtAuthenticator();
jwtAuthenticator.addSignatureConfiguration(new SecretSignatureConfiguration(KEY1));
jwtAuthenticator.addSignatureConfiguration(new SecretSignatureConfiguration(KEY2));
jwtAuthenticator.addEncryptionConfiguration(new SecretEncryptionConfiguration(KEY1));
jwtAuthenticator.validate(new TokenCredentials(token, "FacebookClient"));
Finally, after adding the String generate(Map claims)
method to the JwtGenerator
and the Map validateTokenAndGetClaims(String token)
method to the JwtAuthenticator
, we don’t have any dependency left on the pac4j profile and you now have a full library for JWT.