Modding JJWT
JSON Web Tokens:
According to RFC#7519 JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
JWT contains 3 parts appended in a particular order.
1. Header — containing metadata about the token itself, example : type of token, signing algorithm used (‘NONE’ when not signed)
2. Claims set / payload — The JWT Claims Set represents a JSON object whose members are the claims conveyed by the JWT.
3. Signature — Contains the signature of header and claims set using the algorithm mentioned in Header.
Construction :
H : urlBase64(header)
P : urlBase64 (payload)
S : urlBase64 (signature(H || . || P)) (|| implies concatenation)
Token : H || . || P || . || S
Signature part of the token is optional but it needs to have 3 sections connected by a “period”.
Token Based Authentication
For stateless services, authentication is done by using tokens. These tokens must contain sufficient information to authenticate the sender of the request. Since these are tokens, we also need to verify the authenticity of the issuer. The tokens must be such that no one should be able to forge them and only intended parties can verify it.
Tokens may contain a field of authority to determine access to a resource on a web page or application. It may be ‘user’ or ‘admin’. These tokens are not meant to store any sensitive information as they are just base64 encoded JSONs. (Although they can optionally be encrypted using JWE standard). So it is easy to create a payload. But, one must not be able forge a payload. This is taken care by the signature part of the token.
To obtain a token, the user first needs to supply its user credentials. When validated, the service returns a token with corresponding authentication and authorization information in the payload signed using its asymmetric key. These tokens generally contain the identification of the issuer and identification of the audience to which it was intended. These tokens are recommended to be short lived.
Use Case:
A RESTful service is developed to store sensitive information in the form of encrypted entities. It supports persistence of the entity which can be retrieved/modified by an authorized user. To handle the authorization, JWT tokens signed by an asymmetric key either supplied by the client or generated by the service at the time of registration needs to be presented in the request. This token is verified as a means to authenticate the request.
The Problem:
To build the JWT tokens, we use a java implementation of the specification — JJWT. We construct a token as follows :
String compactJws = Jwts.builder() .setSubject(“Joe”) .signWith(SignatureAlgorithm.HS512, key) .compact();
And to verify a token compactJws
as follows:
try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(compactJws) .getBody(); //OK, we can trust this JWT} catch (SignatureException e) { //don't trust the JWT!}
This builder interface provides a signWith(Algorithm, key)
function that accepts an algorithm from a set of pre-defined and supported algorithms defined in the library and a clear key for signing. PCI DSS 3.0 Guidelines states:
3.5.1: “Restrict access to cryptographic keys to the fewest number of custodians necessary”
3.5.4: “Store cryptographic keys in the fewest possible locations”
Also, the application is designed to segregate roles into layers. So all we should have is a handle to the actual key in the business logic that provides the token. The cryptographic tasks should be segregated into a crypto layer i.e. the business logic must not be concerned with the keys itself.
Similarly, for verification it provides with setSigningKey(Key)
that picks up the algorithm from the header and does the verification. Again, this key too needs to be in clear.
The current concern is, how do we sign with mere handles to actual keys using JJWT as JJWT library has its own provider that uses JCE interface for cryptographic tasks and have specific interfaces to accept input.
At the time of development, the library did not support having custom implementations for Algorithms and providers for delegating encryption to a different service and with no timeline for it, the library had to be modified.
The solution:
The Builder and Parser interfaces need to be modified. 2 new methods are introduced in both the interfaces.
JwtBuilder setCryptoProvider(CryptoProvider cryptoProvider);JwtBuilder setConfigParams(Map<String, Object> config);
The first method allows the library to accept a custom crypto provider that is user specific and can contain whatever logic until it implements the CryptoProvider interface added in the library.
The cryptoProvider
Interface –
It contains two basic methods that is used for token building and verification.
/**** @param plain the string that needs to be signed* @param config configuration hashmap that requires exclusive information required for sign* This config map can be implementation specific as the user will have to implement the CryptoProvider and provide* an instance for using jjwt.* @return String the signed value as a String** Parsing of the config map is left for the implementation*/public String sign(String plain, Map<String, Object> config)throws SignatureException;/**** @param plain the string that has been signed* @param sign the sign that is to be verified for the given plain text* @param config configuration hashmap that requires exclusive information required for verify* This config map can be implementation specific as the user will have to implement the CryptoProvider and provide* an instance for using jjwt.* @return Boolean value identifying if the sign is valid or not*/public Boolean verify(String plain, String sign,Map<String, Object> config) throws SignatureException;
The config params that are passed in the second method added to the interfaces contains users-specific information. This is intended to support a user-specific implementation for cryptographic functions. Hence it is opaque to the library. These config map can be blindly passed to the implementation of the cryptoProvider
. It is on the implementing class how to use it. It can implement its own protocols for token signing, delegating this to another service or to an HSM. All of the metadata required can be stored in the config map.
To actually use these added functions, the code had to be changed in the library so as to override library’s sign/verify call when both cryptoProvider
and configure are explicitly set.
So by supplying a CryptoProvider implementation and relevant config, the default behaviour can be delegated to the application’s own implementation overriding JJWT’s JCE Implementation.
String token = Jwts.builder().setId(ID) .setIssuedAt(now) .setExpiration(exipration) .setCryptoProvider(cryptoProvider) .setConfigParams(config).compact();
config is a Map<String, Object>()
and cryptoProvider
is an instance of a class implementing CryptoProvider
interface from the library.
Similary verification works as follows :
claims = Jwts.parser() .setCryptoProvider(cryptoProvider) .setConfigParams(config) .parseClaimsJws(token).getBody();
With this design change, the cryptographic responsibility can be abstracted out without affecting backwards compatibility.
links : https://github.com/jwtk/jjwt
modded version : https://github.com/prateeknischal/jjwt